Skip to content

Lock the send slider once a broadcast has been attempted - #6190

Draft
peachbits wants to merge 1 commit into
developfrom
matthew/send-broadcast-lock
Draft

Lock the send slider once a broadcast has been attempted#6190
peachbits wants to merge 1 commit into
developfrom
matthew/send-broadcast-lock

Conversation

@peachbits

Copy link
Copy Markdown
Contributor

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

none

Requirements

If you have made any visual changes to the GUI. Make sure you have:

  • Tested on iOS device
  • Tested on Android device
  • Tested on small-screen device (iPod Touch)
  • Tested on large-screen device (tablet)

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 saveTx then threw, and handleSliderComplete treated that throw like any other failure: a generic network error card plus a finally that re-armed the 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 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 broadcastTx is called. Once set, the finally block 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 beforeTransaction hook, 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 calls resetSlider() instead of leaving the slider spinning, which was a pre-existing bug.

SafeSlider gains an explicit lockedText prop for this. Overloading the existing disabled prop was the first attempt and was wrong: a completed slider that a parent then disables would swap its spinner for disabledText, which defaults to "Enter an Amount". That would have affected the other fourteen scenes using SafeSlider whenever a parent disables mid-callback, which SendScene2 itself can do via processingAmountChanged and hasPendingTx.

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. 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:

Scenario Result
Broadcast succeeds, then saveTx throws "No addresses to process" (the incident) Slider locks, post-broadcast copy shown
Broadcast itself throws Slider locks, ambiguous-failure copy shown
Signing throws before any broadcast Slider re-arms to "Slide to Confirm"
Dragging the locked slider across the full track, both directions No movement, no second send

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. tsc and eslint clean.

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.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant