fix(broadcast): bail on confirmed-with-err to avoid CF 504#302
Merged
Conversation
When a tx is confirmed on-chain but the program returns an error (e.g. AccountNotInitialized, slippage exceeded), sendAndConfirmWithRetry used to discard the value.err from confirmTransaction and return the signature as success. If the WS subscription was slow to fire, confirmTransaction would poll until blockhash expiry, exceeding the 100s Cloudflare edge timeout and the user would see a generic CF 504 HTML page instead of a structured error envelope. Two changes implement issue #299 Option 3: 1. confirmInspected wraps connection.confirmTransaction and throws a new TransactionFailedOnChainError when value.err is non-null, capturing both the signature and the err detail. 2. pollForErr runs in parallel, calling getSignatureStatuses every interval and returning the same typed error as soon as the RPC reports a confirmed-with-err status. Either path wins the race; /api/tx/broadcast catches the typed error and returns a structured 502 TX_FAILED_ON_CHAIN with the err payload so the FE can render an actionable message. The poll loop is fire-and-forget on cleanup so successful broadcasts do not pay an extra interval of latency waiting for the poll to observe stopped=true. Closes #299
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This was referenced May 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #299. When a transaction is confirmed on-chain but the program returns an error (e.g.
AccountNotInitialized, slippage exceeded),sendAndConfirmWithRetryused to discardvalue.errfromconfirmTransactionand return the signature as a success. If the WS subscription was slow to fire,confirmTransactionpolled until blockhash expiry (~90s), exceeding the 100s Cloudflare edge timeout — the user saw a generic CF 504 HTML page instead of a structured error envelope.This PR implements Option 3 from the issue:
confirmInspected— wrapsconnection.confirmTransactionand throws a newTransactionFailedOnChainErrorwhenvalue.erris non-null, capturing both the signature and the err detail.pollForErr— runs in parallel, callinggetSignatureStatusesevery interval and returning the same typed error the moment the RPC reports confirmed-with-err. Either path wins the race;/api/tx/broadcastcatches the typed error and returns a structured502 TX_FAILED_ON_CHAINwith the err payload so the FE can render an actionable message.The poll loop is fire-and-forget on cleanup so successful broadcasts do not pay an extra interval (2s in prod) of latency waiting for the poll to observe
stopped=true.Response envelope on a confirmed-with-err tx
Before:
200 { signature }with a signature whose tx errored — FE proceeded as if success.After:
{ "error": { "code": "TX_FAILED_ON_CHAIN", "message": "Transaction <sig> confirmed on-chain but the program returned an error: {\"InstructionError\":[0,{\"Custom\":3012}]}", "signature": "<sig>", "err": { "InstructionError": [0, { "Custom": 3012 }] } } }Test plan
pnpm test -- --runinpackages/agent— 1652 passed (was 1648, +4 new tests: 3 insendWithRetry, 1 intx-broadcast)pnpm typecheckclean across root + sdk + app + agentconfirmTransactionreturns{ value: { err } }→ throws; (b)getSignatureStatusesdetects err first whileconfirmTransactionhangs → throws; (c) poll stays quiet onnull(tx-not-yet-seen) status while confirmation resolves cleanly.routes/tx-broadcasttest asserts the structured502 TX_FAILED_ON_CHAINenvelope shape (code, message, signature, err)./tmp/sipher-smoke/smoke5.mjsonce VPS deploys — expect/api/tx/broadcastto return the new 502 envelope instead of 200 for the confirmed-with-3012 signature.Notes
winner.kind === 'polled' && !winner.errOrNullbranch is structurally unreachable (poll only returns null whenstopped=true, set in finally); the code throws an invariant error if it ever fires.redact()is reused onerr.messageto ensure Helius API-key fragments don't escape if a future code path attaches them.confirmTransactiontypically resolves in ~3s for confirmed-with-err txs, so this is primarily a correctness fix; the parallel poll adds defense-in-depth for the slow-subscription edge case the issue called out.