Skip to content

fix(files): don't pay for uploads cancelled during the quote phase - #194

Merged
Nic-dorman merged 1 commit into
mainfrom
nic/v2-830-cancel-guard
Jul 30, 2026
Merged

fix(files): don't pay for uploads cancelled during the quote phase#194
Nic-dorman merged 1 commit into
mainfrom
nic/v2-830-cancel-guard

Conversation

@Nic-dorman

Copy link
Copy Markdown
Member

Problem (community report)

Cancelled uploads still call the smart contracts.

Verified race: after Approve, startRealUpload sets the row to quoting and awaits the real quote (encrypt + per-chunk quote collection — can take minutes). The row is cancellable in that state: cancelPendingUpload removes it. But startRealUpload never re-checks the row after the await, and updateEntry silently no-ops on removed ids — so when the quote resolves, the flow continues into ensureAllowance + payForQuotes/payForMerkleTree (real ANT + gas; a surprise wallet prompt on the WalletConnect path, and silent signing with no prompt at all on the direct-key path) and then confirm_upload stores the chunks. The result is invisible: no row, no history entry, no datamap surfaced.

Fix

One guard at the paying transition in stores/files.ts::startRealUpload: if the row no longer exists when the quote resolves, bail out before payment. This single check closes the race completely — once paying is set (synchronously after the guard), cancelPendingUpload refuses the row, so there is no later removal window. Cancels during the pre-approval estimate and in the queued states were already safe (no payment reachable).

The daemon's parked PreparedUpload from the aborted quote is reclaimed by the existing gc_pending_uploads sweep on a later start_upload — no Rust changes needed.

Testing

  • New regression test drives the exact race: the mocked backend receives start_upload, the row is cancelled mid-quote, the quote still arrives (payment_required: false, so the pre-fix flow would fall straight through), and the test asserts the flow stops before confirm_upload.
  • npm run test: 43/43 green; npx nuxi typecheck: no app errors.

🤖 Generated with Claude Code

Cancelling a row while its post-approval quote was in flight did not
stop the upload flow. cancelPendingUpload allows cancelling `quoting`
rows and removes the row, but startRealUpload never re-checked the row
after awaiting the quote, and updateEntry silently no-ops on removed
rows - so the flow marched on into a real on-chain payment (approve +
payForQuotes/payForMerkleTree, a wallet prompt on the WalletConnect
path, silent signing on the direct-key path) and chunk storage, for an
upload with no visible row and no history entry.

Guard the paying transition: bail out if the row no longer exists once
the quote resolves. After `paying` is set no further removal is
possible (cancelPendingUpload refuses paying rows), so the single
guard closes the race. The daemon's parked PreparedUpload is reclaimed
by gc_pending_uploads on a later start_upload.

Regression test drives the exact race: cancel arrives while the quote
is in flight, backend still answers, flow must stop before
confirm_upload.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Nic-dorman
Nic-dorman merged commit 54b37bd into main Jul 30, 2026
4 checks passed
@Nic-dorman
Nic-dorman deleted the nic/v2-830-cancel-guard branch July 30, 2026 15:44
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