Skip to content

fix: stop a reset transfer from writing over the next one - #154

Open
JspIIV wants to merge 1 commit into
circlefin:masterfrom
JspIIV:fix/cancel-in-flight-transfer
Open

fix: stop a reset transfer from writing over the next one#154
JspIIV wants to merge 1 commit into
circlefin:masterfrom
JspIIV:fix/cancel-in-flight-transfer

Conversation

@JspIIV

@JspIIV JspIIV commented Aug 20, 2026

Copy link
Copy Markdown

Fixes #153.

The problem

reset() cleared currentStep, logs and error but left executeTransfer running. retrieveAttestation polls in a while (true) loop with no timeout and no cancellation, so an abandoned run kept polling every five seconds and kept calling addLog and setCurrentStep.

Pressing Reset during Waiting for attestation therefore hid the transfer rather than ending it — log lines reappeared on the cleared screen and the step advanced on its own to minting and then completed.

Starting a second transfer made it worse: both runs wrote to the same state, their log lines interleaved, and whichever finished first set the step. The first transfer could display "Bridge completed successfully" while the second was still burning.

The change

The hook now owns an AbortController for the run in flight.

  • executeTransfer aborts any previous run before starting, so two transfers can never write to the same state.
  • reset() aborts before clearing, and an unmount effect aborts too, so a run cannot outlive the page.
  • The signal is threaded through the attestation poll and the mint retry loop, and passed to fetch.
  • A wait() helper replaces the bare setTimeout sleeps so a pending delay gives up immediately instead of holding the run alive for another five seconds.
  • An aborted run is treated as abandoned rather than failed, so it does not paint an error over a screen the user already reset.

Also in the same paths

The attestation wait is bounded. A message that never attests polled forever. It now gives up after three hours — chosen to sit well clear of standard-finality times — and the error says the burn succeeded so the mint can be completed later.

The waiting log no longer grows without bound. Waiting for attestation... was appended on every poll: roughly 700 identical lines after an hour. It is now logged once a minute.

The gas line reported a gas limit as a price.

const gasWithBuffer = (gasEstimate * GAS_BUFFER_PERCENT) / 100n;
addLog(`Gas Used: ${formatUnits(gasWithBuffer, 9)} Gwei`);

gasWithBuffer is a count of gas units, so dividing by 1e9 and labelling it Gwei printed a 180,000 gas limit as 0.00018 Gwei. It was also called Gas Used while being a pre-flight estimate. The limit is now logged as a number, and the real receipt.gasUsed is logged once the mint lands.

Verification

npx tsc --noEmit, npm run lint, npm run build and prettier --check all pass. Prettier touched only the line this PR adds; the rest of the file is unchanged.

I have not been able to exercise this end to end against a live bridge — that needs funded wallets on both chains and a real attestation round trip. The change is deliberately confined to cancellation plumbing and two log lines, with no change to the approve, burn, fee or mint transaction paths. Reviewers who can run a real transfer may want to confirm the reset-mid-attestation case directly.

Happy to adjust the three-hour ceiling, the log interval, or split the gas-line fix into its own PR if you would rather keep this one to the cancellation change.

reset() cleared currentStep, logs and error but left executeTransfer
running. retrieveAttestation polls in a `while (true)` loop with no
timeout and no cancellation, so the abandoned run kept polling every
five seconds and kept calling addLog and setCurrentStep. Pressing Reset
hid the transfer rather than ending it: log lines reappeared on the
cleared screen and the step advanced on its own to minting and then
completed.

Starting a second transfer made it worse, because both runs wrote to the
same state. Their log lines interleaved and whichever finished first set
the step, so the first transfer could display "Bridge completed
successfully" while the second was still burning.

The hook now owns an AbortController per run. reset() and unmount abort
it, executeTransfer abandons any previous run before starting, the
signal is threaded through the attestation poll and the mint retry loop,
and an aborted run is treated as abandoned rather than failed so it does
not paint an error over a screen the user already reset.

Two smaller fixes in the same paths:

- The attestation wait is bounded. A message that never attests polled
  forever; it now gives up after three hours and says the burn
  succeeded so the mint can be completed later.
- "Waiting for attestation..." was appended every poll, leaving roughly
  700 identical lines after an hour. It is logged once a minute.
- The gas line reported a gas limit as a price: formatUnits(gas, 9)
  printed a 180,000 gas limit as "0.00018 Gwei". It now logs the limit
  as a number, and the real receipt.gasUsed once the mint lands.
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.

reset() leaves the transfer running: a stale run can report Bridge completed successfully over a later transfer

1 participant