Repository navigation
fix(wallet-toolbox): a terminal rejection fails the action's pending children and keeps its unnamed inputs - #1080
Open
Calgooon wants to merge 2 commits into
Conversation
…n's children and its live inputs jammed The jam a phone wallet hit on Arcade: a retry R answered RECEIVED (202), then REJECTED 466 "UTXO_SPENT ... H:0 utxo already spent by tx M"; a later action C was funded from R's change and rejected as "parent rejected". The wallet showed C's change (6,654) against 11,657 of change on chain. On a real SQLite store: - the SSE REJECTED event fails R, but C stays unproven and keeps the head M:0 and F:0 allocated, and C's change stays in the balance; - the reconcile poll's terminal verdict fails R and C, but quarantines every input of R, so M:1 (unspent on chain, never named by Arcade) leaves the balance for good. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…children and keeps its unnamed inputs An action Arcade answered RECEIVED (202) and later REJECTED failed alone. An action already funded from its change stayed unproven, kept its other inputs allocated and its change in the balance, until the reconcile poll reached it an hour later; meanwhile the wallet kept funding new actions from that dead change. And an input-conflict rejection quarantined every input of the action, so the change a losing retry had spent left the balance for good although Arcade named a different input as the spent one. - failPendingDescendants: after a request turns terminal, fail every pending local request whose transaction spends one of its outputs, transitively: request invalid with a failedParentCascade note, transaction failed (its inputs released), the outputs taken from a failed parent re-quarantined. Run inside the terminal transition of TaskArcadeSSE and TaskReconcilePendingTransactions; listeners hear REJECTED for each. - quarantineReqInputs takes the rejection's text; when it names outpoints that are inputs of the action (Arcade's "<txid>:<vout> utxo already spent by tx ..."), only those are quarantined. A rejection naming none still quarantines every local input. - Regression on a real SQLite store (the beta jam): after R's REJECTED, by event or by poll, R and its child C are failed, M:0, M:1 and F:0 are spendable, and the change balance is 11,657 (was 6,654 by event, 8,404 by poll). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Calgooon
requested review from
BraydenLangley,
sirdeggen,
tonesnotes and
ty-everett
as code owners
October 14, 2026 18:33
|
This branch has not been deployed
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.



Program and scope
RECEIVED(202) and laterREJECTEDwas failed alone. Actions already funded from its change stayedunproven, holding their other inputs and offering their change, untilTaskReconcilePendingTransactionsreached them an hour later. And an input-conflict rejection quarantined every input of the action, including the change a losing retry spent, although Arcade's text names the spent outpoint. On mainnet a phone wallet showed 6,654 sats against 11,657 on chain (txids in wallet-toolbox: an action REJECTED after RECEIVED leaves the actions funded from its change pending and quarantines its unnamed inputs; the balance does not read the chain #1079).storage/methods/reconcileFailedTransactionInputs.ts):failPendingDescendants(terminalTxids, storage, trx): fails every pending local request (ProvenTxReqNonTerminalStatus) whose raw transaction spends an output of a terminal one, transitively:invalid, with afailedParentCascadehistory note;failedthroughupdateTransactionStatus, which releases its inputs and makes its outputs unspendable;quarantineReqInputsFromFailedParentsre-quarantines the outputs it took from a failed parent.Completed or proven transactions are never touched.
quarantineReqInputs(..., evidence?: { extraInfo }): when the rejection text names<txid>:<vout>outpoints that are inputs of the action, only those local inputs are quarantined. When it names none, every local input is quarantined, as before.TaskArcadeSSE.applyTerminalFailureandTaskReconcilePendingTransactions.applyTerminalVerdictpass the rejection text and run the cascade inside the same storage transaction as the terminal transition. Listeners hearREJECTEDfor each descendant.TaskCheckForProofsignoring an ArcadeREJECTEDit reads while looking for a proof.Impact
Affected packages:
@bsv/wallet-toolbox,@bsv/wallet-toolbox-client,@bsv/wallet-toolbox-mobile(patch). The version bump, release note and changelog entry are intentionally deferred, as on #1078: #1076 takes these packages to 2.14.10, and an independent bump here would conflict with it. I will add them on whichever release carries this change.Verification
8fd9b1c3a),test/monitor/rejectedAfterReceived.test.ts, 2 of 4 failing:expect(await txStatus(jam, jam.rows.C)).toBe('failed'), received"unproven".expect(await output(jam, jam.outputs.M1)).toMatchObject({ spendable: true }), receivedspendable: false.97e0a2452): 4 of 4, balance 11,657 on both paths. "A rejection that names no input of the action still quarantines every local input" passes before and after.src/monitorandtest/monitor: 17 suites, 178 tests pass.TaskArcSSE.test.ts's mock provider gainsfindProvenTxReqs(no pending requests), a method everyStorageProviderhas.npx jestover the package with thetestscript's ignore patterns, afterpnpm build: every suite passes exceptStorageKnexMigrationAtomicity("Child exited before checkpoint"). That suite fails the same way on the base sources in this environment.syncTransferHttp(local HTTP sync) failed after 593 s in a separate run with the fix. Its cause was not examined, and it was not run on the base sources.pnpm lint(package),tsc --noEmit -p tsconfig.json, and Prettier on the changed files: pass.compatibility, public API, artifacts, dependencies, docs, and operations
scope-based skip is expected and validated by the merge gate
Security and dependencies
Release and operations
controlling program (deferred, see Impact)
Migration: none.
🤖 Generated with Claude Code