Lock the send slider once a broadcast has been attempted - #6190
Draft
peachbits wants to merge 1 commit into
Draft
Conversation
One intended Bitcoin send became five real payments (Asana 1217135300337949). Every broadcast succeeded over the HTTP fallback, the engine's saveTx then threw, and the send scene treated that throw as a failed send: a generic network error card plus a re-armed slider. Because saveTx marks the inputs spent before it throws, each retry re-quoted on the remaining UTXOs, so every slide was a fresh real payment. A failure reported by broadcastTx does not prove the transaction is absent from the network. A server can relay it and still fail to answer, and the network conditions that broke the broadcast also make any immediate "did it land?" check unreliable. So the scene now treats the broadcast call as a one-way boundary: - A ref is set immediately before broadcastTx is called. Once set, the finally block never re-arms the slider, on success or on failure, and the slider renders locked as "Send Attempted" for the life of the scene. The ref (rather than state) is what the handler reads, so the FIO no-bundled retry recursion cannot re-arm it either. - Failures at or after that boundary no longer show the generic error. They show "Transaction Status Unknown" telling the user the transaction may have reached the network, to check a block explorer or wait for a confirmation email, and that sending again risks a duplicate payment. Separate copy distinguishes a broadcast that reported failure from an error after a successful one. - Failures before the boundary (PIN, the beforeTransaction hook, the FIO fee check, signing) still show the existing errors and still re-arm the slider, because nothing can have been sent. The hook failure also now resets the slider instead of leaving it spinning. SafeSlider gains an explicit `lockedText` prop for this. Overloading the existing `disabled` prop would have shown a completed slider's `disabledText` (which defaults to "Enter an Amount") in place of the spinner whenever any parent disabled a slider mid-callback, affecting the other fourteen scenes that use it. The locked-state card is longer than a normal error and the slider floats over the bottom of the scroll view, so the existing needsScrollToEnd mechanism scrolls it into view. Verified on the iOS simulator against the real send scene with the broadcast stubbed out, for a post-broadcast throw, an ambiguous broadcast failure, and a pre-broadcast signing failure.
2 tasks
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.
CHANGELOG
Does this branch warrant an entry to the CHANGELOG?
Dependencies
none
Requirements
If you have made any visual changes to the GUI. Make sure you have:
Description
GUI half of the "send failed in the UI but the money moved" incident. Asana: https://app.asana.com/1/9976422036640/project/1213880789473005/task/1217135300337949
Engine companion (independent, neither blocks the other): EdgeApp/edge-currency-plugins#458
One intended Bitcoin send became five real payments. Every broadcast succeeded over the HTTP fallback, the engine's
saveTxthen threw, andhandleSliderCompletetreated that throw like any other failure: a generic network error card plus afinallythat re-armed the slider. BecausesaveTxmarks the inputs spent before it throws, each retry re-quoted on the remaining UTXOs, so every slide was a fresh real payment.A failure reported by
broadcastTxdoes not prove the transaction is absent from the network. A server can relay it and still fail to answer, and the network conditions that broke the broadcast also make an immediate "did it land?" check unreliable. Polling was considered and rejected: it blocks the flow and any timeout is arbitrary. So the scene treats the broadcast call as a one-way boundary instead.The slider is now idempotent. A ref is set immediately before
broadcastTxis called. Once set, thefinallyblock never re-arms the slider, on success or failure, and the slider renders locked as "Send Attempted" for the life of the scene. The ref rather than state is what the handler reads, so the FIO no-bundled retry recursion cannot re-arm it either.Honest messaging replaces the generic error. Failures at or after the boundary show "Transaction Status Unknown", telling the user the transaction may have reached the network, to check a block explorer or wait for a confirmation email, and that sending again could produce a duplicate payment. Separate copy distinguishes a broadcast that reported failure from an error after a successful one. There is no fake happy path and no silent success.
Pre-broadcast failures are unchanged. PIN, the
beforeTransactionhook, the FIO fee check and signing all still show their existing errors and still re-arm the slider, because nothing can have been sent. The hook failure also now callsresetSlider()instead of leaving the slider spinning, which was a pre-existing bug.SafeSlidergains an explicitlockedTextprop for this. Overloading the existingdisabledprop was the first attempt and was wrong: a completed slider that a parent then disables would swap its spinner fordisabledText, which defaults to "Enter an Amount". That would have affected the other fourteen scenes usingSafeSliderwhenever a parent disables mid-callback, whichSendScene2itself can do viaprocessingAmountChangedandhasPendingTx.The locked-state card is longer than a normal error and the slider floats over the bottom of the scroll view, so the existing
needsScrollToEndmechanism scrolls it into view. That is a one-line reuse of the pattern four other handlers already use.Testing
Driven on the iOS simulator against the real send scene, with the broadcast stubbed so nothing could reach the network and using a zero-balance wallet:
saveTxthrows "No addresses to process" (the incident)The third and first cases were exercised on the same scene instance in sequence, so the scene re-armed after the pre-broadcast failure and then locked after the broadcast attempt.
Existing jest suite: 722 passing, including
SendScene2.ui.test.tsx.tscandeslintclean.Known gap
The lock is per scene instance. Backing out to the wallet and tapping Send again gives a fresh slider. That matches the direction agreed for this task, where the friction of re-entering the flow is the point, but it is not a hard guarantee against a determined second send.