Never report a successful broadcast as a failed send - #455
Conversation
UtxoEngineProcessor's updateProgressRatio threw 'No addresses to process' whenever the engine had zero subscribed addresses, which is the state of a wallet whose blockbook sockets are all down. saveTx reaches this code via processUtxos after the transaction is already saved and its inputs marked spent, so the throw turned an already-successful send into a reported failure. Skip the progress update instead; there is no denominator to compute a ratio from without subscribed addresses.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
peachbits
left a comment
There was a problem hiding this comment.
The first commit fixes a real crash, but the return lands in a spot that lets setLookAhead run in a state it previously never reached — see the UtxoEngineProcessor.ts comment. The second commit's verification step is defensible in principle (a node can relay a tx and still error), but as written it is serial, untimed, and unconditional, so it turns a fast send failure into a long one on the money path.
0b5ce06 to
7b6c8e2
Compare
|
Both review points addressed: the saveTx return no longer counts denominator-less calls as progress and setLookAhead's new reachability is guarded by spec assertions (11c8184); the verification step is removed entirely in favor of zero-latency failure classification (7b6c8e2), so the send path rejects as fast as before, with ambiguous transport failures now distinguishable from explicit rejections. |
ServerStates.broadcastTx submits the signed transaction to every connected blockbook (or every NOWNode HTTP fallback) and rejects only when all of them fail, but a server can relay the transaction to the network and still return an error or fail to respond. Before rejecting, query the network for the txid and treat a known transaction as a successful broadcast. Also stop throwing on a mismatched broadcast-response txid in UtxoEngine.broadcastTx: the transaction is already on the network at that point, so log a warning instead of reporting a send failure.
7b6c8e2 to
b5255c0
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b5255c0. Configure here.
The immediate network query for the txid was unsound: it runs under the same network conditions that made the broadcast ambiguous, so a negative result proves nothing (the relayed transaction has had no time to propagate) and a positive mostly fires when the network is healthy. It also added serial, untimed network calls to the send path. Classify the exhausted broadcast instead, with no added network calls: collect every server's failure and reject with the original error when all of them are explicit Blockbook rejections (definitively failed, safe to retry), or with BroadcastAmbiguityError when any failure is a transport error, since one of those servers may have relayed the transaction before failing to answer. The GUI can branch on the error name to lock the retry path for ambiguous failures.
b5255c0 to
3a1ab78
Compare



CHANGELOG
Does this branch warrant an entry to the CHANGELOG?
Dependencies
none
Description
Technical design doc
Fixes the engine half of Edge bug "Send - multiple transactions after network error": one intended Bitcoin send became five real payments because every failure the user saw happened AFTER a successful broadcast.
Asana: https://app.asana.com/1/9976422036640/project/1213843652804305/task/1217135300337949
Two defects fixed:
saveTxfailed on a disconnected engine.updateProgressRatiothrewNo addresses to processwhenever zero addresses were subscribed (exactly the state of a wallet whose blockbook sockets are down), andsaveTxreaches it viaprocessUtxosafter the transaction is already saved and its inputs marked spent. The throw is progress bookkeeping with no denominator, not a data error, so it is now a no-op instead of failing the caller's data write. Regression test:saveTxon a never-connected engine resolves (test/common/utxobased/engine/saveTx.spec.ts, red before the fix with the exact incident stack).Broadcast failure was ambiguous.
ServerStates.broadcastTxmulticasts to every connected blockbook (or every NOWNode HTTP fallback) and rejects only when all fail, but a server can relay the transaction and still return an error or time out, so "all servers errored" does not prove the transaction is absent from the network. The exhausted broadcast is now CLASSIFIED, with no added network calls: an already-known rejection (already in block chain / txn-already-in-mempool / txn-already-known) resolves the broadcast as a SUCCESS, since the server's own answer proves the transaction reached the network; when every relay-capable failure is an explicit Blockbook rejection the engine rejects with the original error (definitively failed, safe to retry); when any failure is a transport error it rejects withBroadcastAmbiguityError, which the GUI will use to lock the retry path and show may-have-worked messaging (tracked separately). Electrum stub refusals, which provably never sent anything, are excluded from the determination. An earlier revision queried the network for the txid before rejecting; that was removed after review, since an immediate query runs under the same network conditions that made the broadcast ambiguous and proves nothing either way. The post-broadcast txid-mismatch throw inUtxoEngine.broadcastTx(dead code today, failure-after-success if ever revived) now logs a warning and returns the transaction with the network's txid so the wallet tracks what was actually accepted.The GUI half (send scene must not present post-broadcast errors as failed sends) is the EdgeApp/edge-react-gui companion PR on branch
jon/send-post-broadcast-failure.Note
High Risk
Changes UTXO send/broadcast and saveTx behavior on the payment path; incorrect classification could still allow duplicate sends or block legitimate retries.
Overview
Fixes cases where a successful broadcast was still reported as a failed send, which led to duplicate payments when users retried.
saveTxon a disconnected engine no longer fails after the transaction is already saved. When no addresses are subscribed,updateProgressRatioskips progress updates instead of throwingNo addresses to process.stop()resets progress counters so leftover counts cannot emit a bogus fully-synced state during latersaveTx-driven UTXO work.Exhausted multicasts in
ServerStates.broadcastTxare classified via newbroadcastErrorhelpers: all explicit Blockbook rejections still reject with the original error (safe to retry); any transport-style failure rejects withBroadcastAmbiguityErrorso the UI can treat the send as possibly on-network. Already-known mempool/chain rejections are treated as success.UtxoEngine.broadcastTxlogs and returns the network’s txid on mismatch instead of throwing after relay.Tests cover classification (
broadcastError.spec.ts) and disconnected-enginesaveTx(saveTx.spec.ts).Reviewed by Cursor Bugbot for commit 51e5b66. Bugbot is set up for automated code reviews on this repo. Configure here.