Skip to content

Commit 66ec309

Browse files
authored
docs: Stellar troubleshooting + error reference (#56)
* docs: add stellar troubleshooting guide for common transaction and contract errors * docs: add privacy and stellar troubleshooting guides to documentation index * feat: add stellar-troubleshooting guide and reformat page lists in docs.json * docs: fix indentation in stellar documentation and add troubleshooting section
1 parent 12a9015 commit 66ec309

4 files changed

Lines changed: 429 additions & 101 deletions

File tree

docs.json

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,11 @@
6060
"groups": [
6161
{
6262
"group": "Overview",
63-
"pages": ["introduction", "getting-started", "roadmap"]
63+
"pages": [
64+
"introduction",
65+
"getting-started",
66+
"roadmap"
67+
]
6468
},
6569
{
6670
"group": "Architecture",
@@ -72,7 +76,13 @@
7276
},
7377
{
7478
"group": "Contracts",
75-
"pages": ["contracts/evm", "contracts/stellar", "contracts/solana", "contracts/ckb", "reference/stellar-event-schemas"]
79+
"pages": [
80+
"contracts/evm",
81+
"contracts/stellar",
82+
"contracts/solana",
83+
"contracts/ckb",
84+
"reference/stellar-event-schemas"
85+
]
7686
}
7787
]
7888
},
@@ -81,11 +91,19 @@
8191
"groups": [
8292
{
8393
"group": "SDK",
84-
"pages": ["sdk/overview", "sdk/agent-client"]
94+
"pages": [
95+
"sdk/overview",
96+
"sdk/agent-client"
97+
]
8598
},
8699
{
87100
"group": "Chain Primitives",
88-
"pages": ["sdk/chains/evm", "sdk/chains/stellar", "sdk/chains/solana", "sdk/chains/ckb"]
101+
"pages": [
102+
"sdk/chains/evm",
103+
"sdk/chains/stellar",
104+
"sdk/chains/solana",
105+
"sdk/chains/ckb"
106+
]
89107
}
90108
]
91109
},
@@ -100,6 +118,7 @@
100118
"guides/multichain-agent",
101119
"guides/bring-your-own-model",
102120
"guides/privacy-best-practices",
121+
"guides/stellar-troubleshooting"
103122
"guides/spectre-stellar-cookbook"
104123
]
105124
},
@@ -118,7 +137,10 @@
118137
"groups": [
119138
{
120139
"group": "API Reference",
121-
"pages": ["api-reference/endpoints", "api-reference/types"]
140+
"pages": [
141+
"api-reference/endpoints",
142+
"api-reference/types"
143+
]
122144
}
123145
]
124146
}
@@ -127,4 +149,4 @@
127149
"footerSocials": {
128150
"github": "https://github.com/wraith-protocol"
129151
}
130-
}
152+
}

getting-started.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,4 +149,5 @@ try {
149149
- [Single-Chain Agent Guide](guides/single-chain-agent) — deeper walkthrough
150150
- [Multichain Agent Guide](guides/multichain-agent) — deploy across multiple chains
151151
- [Bring Your Own Model](guides/bring-your-own-model) — use OpenAI or Claude instead of Gemini
152+
- [Stellar Troubleshooting](guides/stellar-troubleshooting) — fixes for common Stellar, Soroban, and Stealth errors
152153
- [SDK Reference](sdk/agent-client) — full API documentation

guides/stellar-troubleshooting.mdx

Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
---
2+
title: "Stellar Troubleshooting Guide"
3+
description: "A comprehensive reference for common Stellar, Soroban, and Stealth errors"
4+
---
5+
6+
_Last Verified: June 24, 2026_
7+
8+
When building on Stellar or Soroban with Wraith, you might encounter opaque errors. This guide catalogs the most common errors, what they mean, and how to fix them.
9+
10+
## Account & Balance Errors
11+
12+
### 1. `tx_bad_seq`
13+
**Meaning**: The transaction's sequence number does not match the account's current sequence number on the ledger.
14+
**Cause**: Sending multiple transactions from the same account in parallel without incrementing the sequence number correctly, or a previous transaction failed/was dropped but the sequence number was incremented locally.
15+
**Fix**: Refetch the account from Horizon/RPC to get the current sequence number before building the transaction.
16+
```typescript
17+
const account = await server.loadAccount(publicKey);
18+
// Build transaction using the fresh account object
19+
const tx = new StellarSdk.TransactionBuilder(account, { fee: "100" })
20+
// ...
21+
.build();
22+
```
23+
24+
### 2. `op_underfunded`
25+
**Meaning**: The account does not have enough XLM (or the specific asset) to execute the operation.
26+
**Cause**: Attempting to send more funds than the account holds, or not accounting for the base reserve and transaction fees.
27+
**Fix**: Check the account balance and ensure it is greater than the transfer amount + fees + base reserve.
28+
```typescript
29+
const account = await server.loadAccount(publicKey);
30+
const xlmBalance = account.balances.find(b => b.asset_type === 'native').balance;
31+
console.log(`Available XLM: ${xlmBalance}`);
32+
// Ensure transferAmount < (xlmBalance - baseReserve - fees)
33+
```
34+
35+
### 3. `op_low_reserve` / Base Reserve Below Threshold
36+
**Meaning**: The operation would drop the account's balance below the minimum required base reserve.
37+
**Cause**: Creating new trustlines, signers, or data entries requires an additional base reserve (currently 0.5 XLM per entry).
38+
**Fix**: Send additional XLM to the account to cover the reserve for the new entries.
39+
```typescript
40+
// Fund the account with extra XLM for the new trustline
41+
const fundTx = new StellarSdk.TransactionBuilder(sourceAccount, { fee: "100" })
42+
.addOperation(StellarSdk.Operation.payment({
43+
destination: targetPublicKey,
44+
asset: StellarSdk.Asset.native(),
45+
amount: "2.0" // Cover the 0.5 XLM reserve per entry + fees
46+
}))
47+
.setTimeout(30)
48+
.build();
49+
```
50+
51+
### 4. `op_no_destination` / Account Not Found
52+
**Meaning**: The destination account does not exist on the ledger.
53+
**Cause**: Attempting a standard `payment` operation to an unfunded or non-existent account instead of `createAccount`.
54+
**Fix**: If the account doesn't exist, use `createAccount` instead of `payment`.
55+
```typescript
56+
try {
57+
await server.loadAccount(destinationKey);
58+
// Account exists, use standard payment
59+
} catch (e) {
60+
// Account doesn't exist, use createAccount
61+
builder.addOperation(StellarSdk.Operation.createAccount({
62+
destination: destinationKey,
63+
startingBalance: "5.0"
64+
}));
65+
}
66+
```
67+
68+
## Network Errors
69+
70+
### 5. `429 Too Many Requests` (Friendbot / Horizon)
71+
**Meaning**: You are hitting the rate limits for Friendbot or the public Horizon nodes.
72+
**Cause**: Requesting too many testnet funds or querying Horizon too frequently.
73+
**Fix**: Add exponential backoff, batch your requests, or use a private RPC/Horizon instance.
74+
```typescript
75+
async function fundWithRetry(publicKey, retries = 3) {
76+
for (let i = 0; i < retries; i++) {
77+
try {
78+
await fetch(`https://friendbot.stellar.org?addr=${publicKey}`);
79+
return;
80+
} catch (e) {
81+
if (i === retries - 1) throw e;
82+
await new Promise(res => setTimeout(res, 1000 * Math.pow(2, i))); // Exponential backoff
83+
}
84+
}
85+
}
86+
```
87+
88+
### 6. `502 Bad Gateway` / `504 Gateway Timeout` (Horizon)
89+
**Meaning**: The Horizon server is down, overloaded, or unreachable.
90+
**Cause**: Transient network issues or node maintenance.
91+
**Fix**: Implement retry logic and ensure you have fallback Horizon/RPC URLs configured.
92+
```typescript
93+
const primaryServer = new StellarSdk.Server('https://horizon.stellar.org');
94+
const fallbackServer = new StellarSdk.Server('https://your-custom-horizon.com');
95+
96+
try {
97+
await primaryServer.submitTransaction(tx);
98+
} catch (e) {
99+
if (e.response && (e.response.status === 502 || e.response.status === 504)) {
100+
await fallbackServer.submitTransaction(tx);
101+
}
102+
}
103+
```
104+
105+
### 7. `Soroban RPC: retention window exceeded`
106+
**Meaning**: The requested historical data is no longer available on the RPC node.
107+
**Cause**: Querying events or transactions that occurred before the node's configured retention window.
108+
**Fix**: Use an archiver node or data indexer like Hubble to fetch historical data.
109+
```typescript
110+
// Instead of querying Soroban RPC for old events, query an indexer API
111+
const response = await fetch(`https://indexer.example.com/events?contract=${contractId}`);
112+
const oldEvents = await response.json();
113+
```
114+
115+
### 8. `tx_too_late`
116+
**Meaning**: The transaction was submitted after its specified timebounds expired.
117+
**Cause**: Network congestion delayed the transaction, or the timebound maxTime was set too short.
118+
**Fix**: Increase the maximum timebound limit.
119+
```typescript
120+
const tx = new StellarSdk.TransactionBuilder(account, { fee: "100" })
121+
// ... operations
122+
.setTimeout(300) // Increase from default to 300 seconds (5 minutes)
123+
.build();
124+
```
125+
126+
## Signing Errors
127+
128+
### 9. `Freighter not installed`
129+
**Meaning**: The web app cannot detect the Freighter wallet extension.
130+
**Cause**: User has not installed Freighter, or the extension has not injected the `freighterApi` into the page yet.
131+
**Fix**: Check `isConnected()` and prompt the user to install Freighter if unavailable.
132+
```typescript
133+
import { isConnected } from '@stellar/freighter-api';
134+
135+
const connected = await isConnected();
136+
if (!connected) {
137+
alert("Please install the Freighter browser extension to continue.");
138+
window.open("https://freighter.app", "_blank");
139+
}
140+
```
141+
142+
### 10. `Network mismatch`
143+
**Meaning**: The transaction is intended for one network (e.g., Testnet) but Freighter is connected to another (e.g., Public).
144+
**Cause**: User switched networks in their wallet, or the dApp didn't specify the correct network.
145+
**Fix**: Request the correct network before signing.
146+
```typescript
147+
import { getNetwork, signTransaction } from '@stellar/freighter-api';
148+
149+
const network = await getNetwork();
150+
if (network !== 'TESTNET') {
151+
alert("Please switch your wallet to the Stellar Testnet.");
152+
return;
153+
}
154+
const signedTx = await signTransaction(xdr, { network: 'TESTNET' });
155+
```
156+
157+
### 11. `User rejected signature`
158+
**Meaning**: The user clicked "Reject" in the wallet popup.
159+
**Cause**: Normal user behavior, or the user didn't recognize the transaction.
160+
**Fix**: Catch the specific rejection error and handle it gracefully in the UI.
161+
```typescript
162+
try {
163+
const signedTx = await signTransaction(xdr, { network: 'PUBLIC' });
164+
} catch (e) {
165+
if (e.message.includes("User declined")) {
166+
console.log("Transaction cancelled by user.");
167+
showToast("Transaction was cancelled.");
168+
} else {
169+
throw e;
170+
}
171+
}
172+
```
173+
174+
### 12. `tx_bad_auth` / `op_bad_auth`
175+
**Meaning**: The transaction lacks the required signatures for its operations.
176+
**Cause**: The transaction was modified after being signed, or a required multi-sig signer is missing.
177+
**Fix**: Ensure all necessary parties sign the exact final transaction hash.
178+
```typescript
179+
// Multi-sig scenario: ensure both signers sign the SAME transaction object
180+
tx.sign(keypair1);
181+
tx.sign(keypair2); // Make sure the weight meets the threshold
182+
await server.submitTransaction(tx);
183+
```
184+
185+
## Stealth-Specific Errors
186+
187+
### 13. `Derived address matches recipient`
188+
**Meaning**: The calculated stealth address is identical to the recipient's public key.
189+
**Cause**: The recipient's scan key or spend key wasn't properly configured, or entropy generation failed.
190+
**Fix**: Ensure cryptographically secure random entropy is used when deriving the ephemeral key.
191+
```typescript
192+
import { randomBytes } from 'crypto';
193+
import { deriveStealthAddress } from '@wraith/stealth';
194+
195+
const entropy = randomBytes(32); // Use true randomness
196+
const stealthInfo = deriveStealthAddress(recipientMeta, entropy);
197+
if (stealthInfo.address === recipientMeta.publicKey) {
198+
throw new Error("Invalid stealth derivation");
199+
}
200+
```
201+
202+
### 14. `Zero-balance scan returning matches`
203+
**Meaning**: The stealth scan function is finding addresses that belong to the user, but they have no balance.
204+
**Cause**: Dusting attacks, or previous stealth payments were fully spent but the ledger still shows the account.
205+
**Fix**: Filter scan results to only include accounts with a balance greater than 0 (or base reserve).
206+
```typescript
207+
const matches = await stealthScanner.scan(startLedger, endLedger);
208+
const activeMatches = await Promise.all(
209+
matches.map(async (match) => {
210+
const account = await server.loadAccount(match.address);
211+
const balance = parseFloat(account.balances[0].balance);
212+
return balance > 0 ? match : null;
213+
})
214+
);
215+
const validMatches = activeMatches.filter(m => m !== null);
216+
```
217+
218+
### 15. `Name resolution null` (Federation)
219+
**Meaning**: A Stellar Federation address (e.g., `user*wraith.com`) could not be resolved to an account ID.
220+
**Cause**: The federation server is down, or the user does not exist on that domain.
221+
**Fix**: Fall back to manual address entry or retry the federation lookup.
222+
```typescript
223+
try {
224+
const record = await StellarSdk.FederationServer.resolve('alice*example.com');
225+
return record.account_id;
226+
} catch (e) {
227+
console.error("Federation resolution failed, please enter the raw G-address");
228+
promptForRawAddress();
229+
}
230+
```
231+
232+
### 16. `Stealth payload too large for memo`
233+
**Meaning**: The stealth ephemeral public key or metadata exceeds the 32-byte limit of a Stellar `Memo.hash`.
234+
**Cause**: Attempting to attach uncompressed keys or extra data in the memo field.
235+
**Fix**: Use compressed public keys or store extra metadata in Soroban contract state/events instead.
236+
```typescript
237+
// Ensure the ephemeral key is 32 bytes
238+
const ephemeralKeyBuffer = getCompressedKey(ephemeralPublicKey);
239+
const tx = new StellarSdk.TransactionBuilder(account, { fee: "100" })
240+
.addMemo(StellarSdk.Memo.hash(ephemeralKeyBuffer.toString('hex')))
241+
// ...
242+
.build();
243+
```
244+
245+
## Soroban Contract Errors
246+
247+
### 17. `HostError: Error(Contract, #)` / Contract Trapped
248+
**Meaning**: The smart contract executed a `panic!` or returned a specific error code.
249+
**Cause**: A contract assertion failed (e.g., unauthorized caller, arithmetic overflow).
250+
**Fix**: Check the Soroban CLI or RPC logs for the exact error code and match it to the contract's source code.
251+
```rust
252+
// In your Soroban contract:
253+
#[contracterror]
254+
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
255+
#[repr(u32)]
256+
pub enum Error {
257+
Unauthorized = 1,
258+
InsufficientBalance = 2,
259+
}
260+
// If you see Error(Contract, 2), it's InsufficientBalance.
261+
```
262+
263+
### 18. `op_no_trust` / Missing Trustline
264+
**Meaning**: The Soroban contract attempted to send a Classic asset (like USDC) to an account that doesn't trust it.
265+
**Cause**: The recipient has not established a trustline for the asset being sent by the contract.
266+
**Fix**: Have the recipient submit a `ChangeTrust` operation for the asset before invoking the contract.
267+
```typescript
268+
// Recipient must submit this transaction first
269+
const tx = new StellarSdk.TransactionBuilder(recipientAccount, { fee: "100" })
270+
.addOperation(StellarSdk.Operation.changeTrust({
271+
asset: new StellarSdk.Asset('USDC', usdcIssuerKey)
272+
}))
273+
.build();
274+
```
275+
276+
### 19. `Expired auth` / `auth_invalid`
277+
**Meaning**: The Soroban authorization payload is invalid or has expired.
278+
**Cause**: A time-bound authorization signature (`SorobanAuthorizationEntry`) expired before the transaction was submitted.
279+
**Fix**: Re-sign the authorization payload with a fresh expiration ledger.
280+
```typescript
281+
// When generating the Soroban auth payload, extend the valid ledger range
282+
const currentLedger = await getLatestLedger();
283+
const auth = createSorobanAuth({
284+
validUntilLedger: currentLedger + 100, // Valid for ~10 minutes
285+
// ...
286+
});
287+
```
288+
289+
### 20. `Replay rejection` / `nonce_already_used`
290+
**Meaning**: The contract invocation was rejected because its unique nonce was already used.
291+
**Cause**: Submitting the same signed Soroban payload twice.
292+
**Fix**: Query the contract for the latest nonce for the user, and increment it for the new invocation.
293+
```typescript
294+
// Always fetch the latest nonce before building the Soroban invocation
295+
const nextNonce = await myContract.getNonce({ user: userAddress });
296+
const invocation = await myContract.myFunction({
297+
caller: userAddress,
298+
nonce: nextNonce,
299+
// ...
300+
});
301+
```

0 commit comments

Comments
 (0)