Failure reference
The failure kinds a close can end with, what Retry does with each, and the typed errors the codebase raises.
A close that stops ends with a FailureKind (src/lib/orchestrator/failures.ts). The kind decides
what Retry does:
- Retryable: Retry runs the same plan again with the wallet connected now. Steps that already landed are skipped, and failed or skipped steps are reset and run again. A submission whose outcome was unknown is looked up first.
- Not retryable: Retry rebuilds the plan from fresh discovery, so you review it again.
A Retry without a connected wallet also rebuilds.
Failure kinds
| Kind | Raised when | Retryable |
|---|---|---|
bad_seq | The account's sequence moved under a step (tx_bad_seq) | Yes |
changed_footprint | The ledger entries a Soroban step declared changed or were archived before it landed | Yes |
network | Horizon, RPC or the mediator did not answer usefully: a timeout, a rate limit, a 5xx, an unreadable error page, tx_too_late, tx_insufficient_fee, unreadable fee stats, a Soroban RPC that kept answering TRY_AGAIN_LATER, or a DeFi protocol that could not be read before the merge | Yes |
signing | The wallet did not sign | Yes |
ambiguous | A submission's outcome is unknown; Retry checks it on chain before signing anything again | Yes |
forward_pending | The merge into the forwarding account landed but the forward did not; Retry runs the forward | Yes |
interrupted | You stopped the close at a step boundary | Yes |
close_running | Another run of the close for this account is still going, in this tab or another | Yes |
not_mergeable | The account can no longer be merged as planned (account_merge blocked: ...), the destination can no longer receive a balance you chose to send as-is (Your destination can no longer receive ...), or Horizon refused a transaction with a code a replay cannot fix | No |
position_open | A DeFi position is still open at the merge, or a token could be neither converted nor sent as-is | No |
reverted | A Soroban step failed on chain | No |
unsafe | A step's transaction asked for more than the step needs (allow-list, authorization tree, exact shape or fee cap) and was not signed | No |
bundle_stale | A collected shared-account bundle can no longer land, or Horizon refused it; signatures are collected again | No |
mediator_expired | The forwarding authorization ran too low to sign the merge safely | No |
conversion_refused | A conversion would now sell over the price-impact cap or below the reviewed quote, or has no route; the rebuilt plan asks you per token | No |
claim_unavailable | A Blend backstop emissions claim that simulated at review can no longer run; the rebuilt plan asks you to forfeit the BLND | No |
unknown | Anything unclassified | No |
How an error gets its kind
classifyFailure resolves the kind in this order:
- A
CloseFailurecarries its kind. - A Horizon rejection with result codes:
tx_bad_seqisbad_seq;tx_too_lateandtx_insufficient_feearenetwork, because a rebuild with a fresh time bound or a higher bid can land; every other code isnot_mergeable. - A Horizon
NetworkError(not a 404) with status 429, a 5xx, or no readable status isnetwork. - The message, lowercased, matched in this order:
| # | Phrases | Kind |
|---|---|---|
| 1 | tx_bad_seq, txbadseq | bad_seq |
| 2 | position(s) still open | position_open |
| 3 | account_merge blocked | not_mergeable |
| 4 | footprint, restorepreamble, entryarchived, entry_archived, entry_expired | changed_footprint |
| 5 | failed on-chain | reverted |
| 6 | timed out, timeout of, fetch failed, failed to fetch, network error, networkerror, econnreset, econnrefused, 429, 502, 503, 504, bad gateway, service unavailable, gateway timeout | network |
| 7 | Anything else | unknown |
The diverged-state stops (2 and 3) are checked before the footprint phrases, so a merge refusal whose detail mentions an archived entry is never retried as a footprint race.
Merge refusals
The close loop refuses the merge with messages that start account_merge blocked:. Each is
not_mergeable, except the ones below:
Message after account_merge blocked: | Kind | What Retry does |
|---|---|---|
N Soroban DeFi position(s) still open (blend=…, backstop=…, aquarius=…, …) | position_open | Rebuilds the plan, so the open positions are unwound |
could not confirm your DeFi positions are all closed (…) | network | Runs the same plan once the protocol can be read |
could not re-check DeFi positions before the merge (…) | network | Runs the same plan once discovery answers |
could not read the balance of token … paid out by your DeFi exits (…) | network | Runs the same plan once the token's balance can be read |
N credit balance(s) can't be disposed of as reviewed (…), when any listed balance has a market but its price moved past the review or over the price-impact cap | conversion_refused | Rebuilds the plan and asks for each balance: sell at the new price, send it as-is or return it to its issuer |
The three network refusals are safety stops: a position or payout the close cannot read is never
merged around and stranded.
Typed errors
| Error | Raised by | Carries |
|---|---|---|
CloseFailure | The executor and the execute actor | kind |
AmbiguousSubmitError | A submission Horizon or RPC never confirmed either way | txHash, maxTime; kind ambiguous |
SubmitRejectedError | A classic submission Horizon refused | codes, httpStatus |
AllowlistViolation | The contract allow-list | violations, every blocked invocation with its reason |
UnsafeDefiAuthError | The DeFi authorization-tree guard | nodeKind and a reason |
UnsafeRevokeError, UnsafeTransferError | The revoke and token-transfer guards | A reason, prefixed Refusing to sign revoke: or Refusing to sign token transfer: |
SorobanFeeCapError | The Soroban fee cap | feeClass and the fee breakdown |
SignedEnvelopeError | The check on a wallet's signed envelope | A reason |
LedgerSigningError | A Ledger refusal | refusal: rejected, hash-signing-disabled, too-large or unparseable |
PriceImpactRefusedError | A conversion over the price-impact cap or worse than the reviewed quote | reason, priceImpactBps, expectedOut |
NoSoroswapRouteError | A held-token conversion with no router path to XLM | A message |
BackstopEmissionsClaimError | A backstop emissions claim that no longer simulates | poolId, accruedEmissions |
SimulationError | Soroban simulation | errorCode, diagnostic events |
SimulationFailedError, NodeNotBuiltError | Plan simulation | The node and the upstream error |
SorobanNotQueuedError | An RPC that did not queue a Soroban transaction | status, resultCode |
AccountNotFoundError | The account audit, on a Horizon 404 | publicKey |
AllowanceScanIncompleteError, AllowanceScanOverflowError | An allowance scan that did not cover its window, or a contract cut to 20 spenders | The partial list and what was left out |
TooManySignaturesError | An envelope already at the 20-signature limit | A message |
PendingForwardStorageError | Browser storage refusing a forward recovery record | A message; the close does not start |
SlippageConfigError | A set but invalid NEXT_PUBLIC_SLIPPAGE_BPS or NEXT_PUBLIC_MIN_MARKET_OUT_STROOPS | A message; the build fails |
ClientIpUnavailableError | A TRUSTED_PROXY_HOPS or proxy setup that cannot identify the client | A message; the server refuses to start, or the route answers 500 |
BodyTooLargeError | A request body over its route's cap | limit |
Reading an unknown error
errorMessage(err, fallback) in src/lib/errors.ts returns, in order: an Error's non-empty
message, a non-empty string, the non-empty string message of a plain object, then the fallback,
or String(err) when there is none.
The plain-object case exists because the Soroban RPC client rejects with a { code, message }
object rather than an Error, so an instanceof Error check alone discards the real message.
Slippage and price bounds
| Constant | Value | Meaning |
|---|---|---|
DEFAULT_SLIPPAGE_BPS | 100 | 1% below the quote is the minimum accepted output |
MIN_SLIPPAGE_BPS, MAX_SLIPPAGE_BPS | 10, 500 | The range NEXT_PUBLIC_SLIPPAGE_BPS accepts |
MAX_PRICE_IMPACT_BPS | 200 | A full-size conversion more than 2% below the reference rate needs your decision |
REFERENCE_TRADE_DIVISOR | 1000 | The reference trade is 1/1000 of the balance |