Skip to content

Let senders retry a dashboard pay-out whose update or start failed after the wallet broadcast - #1401

Merged
ebma merged 7 commits into
stagingfrom
fix/dashboard-offramp-post-broadcast-retry
Oct 6, 2026
Merged

ebma merged 7 commits into
stagingfrom
fix/dashboard-offramp-post-broadcast-retry

Conversation

@ebma

@ebma ebma commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

Why

On a dashboard pay-out (SELL), the connected wallet broadcasts the squidRouter transactions before the final /ramp/update reports their hashes and /ramp/start runs. If either call failed (the maintenance guard's 503, a network error), the machine went straight to Failed. The hashes were never recorded, the tokens sat on the ephemeral account, and recovery meant digging through the localStorage ephemeral backup by hand. #1398 narrows the window with a maintenance check before signing, but the gap between the broadcast and the final update/start remains.

What changes

  • Signing and submission are separate steps. signUserTransactions now only drives the wallet and returns the signatures and hashes. A new SubmittingUserTxs state sends them to /ramp/update. A failed update therefore no longer throws away what the wallet produced.
  • New AwaitingRetry state (SELL only). A failed post-broadcast update or start keeps the ramp, plus any signatures and hashes the API has not accepted yet. RETRY resends only the call that failed: the update if it is still owed, otherwise just the start. It never signs or broadcasts again. BUY behaviour is unchanged: a failed start still goes back to AwaitingPayment, and a failed Monerium permit update still goes to Failed, because nothing has moved yet at that point.
  • Survives a reload. The owner-scoped recovery snapshot is now written in SubmittingUserTxs, Starting and AwaitingRetry, and includes the unsubmitted wallet output. On restore, a SELL snapshot resumes in AwaitingRetry. Snapshots that are malformed, or a BUY snapshot that carries wallet output, are discarded as before.
  • Start deadline shown in the UI. Once the wallet has broadcast, the pay-out form is replaced by a status panel: "Starting your transfer…" while a call is in flight, and on failure the error, a Try again button and a countdown to the ramp's expiresAt (the API's 15-minute RAMP_START_EXPIRATION_TIME_SECONDS). After the deadline, retry is hidden. If /ramp/update had already accepted the source hash of a non-AlfredPay pay-out (BRL in practice), the panel says the transfer starts automatically within about 10 minutes (the Start funded sell ramps that the client never started #1394 recovery worker does that) and not to pay again. Otherwise it shows the ramp ID for support. Both views ask the user to check Transactions before sending the payment again, warn against clearing browser data (the ephemeral keys live there), and offer to start a new transfer. The transactions page offers Resume transfer for this state.
  • Switching identity is blocked during SubmittingUserTxs, the same as during SigningUserTxs.

Tests

  • transfer.machine.test.ts uses the real signUserTransactions with a counting userSigning mock. A failed update keeps the hash and resends the identical submission with one broadcast in total. A failed start retries only the start. A restored snapshot resumes at the update it still owes without touching the wallet. Other-owner RETRY is ignored. The owner-activation blocking test now also covers SubmittingUserTxs.
  • transferActor.test.ts: a SELL snapshot restores into AwaitingRetry and stays persisted. A corrupt submission is discarded, and so is a BUY snapshot that carries wallet output.
  • e2e transfer-mxn-journey.spec.ts: the hash update gets a 503 from the maintenance guard, then the page reloads and Try again sends an update identical to the failed one, followed by start and navigation. The wallet stub counts eth_sendTransaction in sessionStorage, and the test asserts exactly one broadcast across the reload. The same test fails on staging source.
  • domain/transfer.test.ts: offrampStartsAfterDeadline matches the recovery worker's rule (non-domestic output currency and an accepted source hash).
  • After merging staging: bun run test (174 pass), bun typecheck, Biome, and the full dashboard Playwright suite (79 pass).

Notes for review

  • Staging (including Show maintenance windows in the dashboard and pause transfers #1398) is merged in. The maintenance pre-check stays at the top of signUserTransactions, and useActiveMaintenance() runs before the new early return in TransferForm.
  • Still open, out of scope here:
    • A partially broadcast wallet sequence (an approve is sent, then a later swap is rejected) still ends in Failed.
    • If a start succeeded on the server but its response was lost, the retry gets a 409 and stays in AwaitingRetry; the existing BUY retry path has the same gap. After the deadline the panel tells the user to check Transactions before paying again.
    • A maintenance window longer than the 15-minute start deadline still needs manual recovery for AlfredPay pay-outs and for pay-outs whose hash update never went through, because the API does not extend the deadline during maintenance. Since Start funded sell ramps that the client never started #1394 the API starts the others itself after the deadline.
  • docs/product-dashboard.md, docs/operations-testing.md, and the dashboard-snapshot note in docs/security-spec/02-signing-keys/ephemeral-accounts.md are updated. The snapshot now holds user-signed permits and hashes, never ephemeral secrets.

ebma added 4 commits October 5, 2026 12:20
…rt fails

Once the wallet has broadcast a SELL's squidRouter transactions the funds sit on
the ephemeral account, so a failed /ramp/update or /ramp/start (for example a
maintenance-window 503) must not drop the ramp or the hashes. Signing now hands
its output to a separate submit step, failures land in AwaitingRetry, and the
recovery snapshot carries the unsubmitted output across reloads.
… deadline

The API refuses to start a ramp 15 minutes after registration, so the retry
panel counts down to expiresAt and, once it passes, gives the ramp ID for a
support-led recovery instead of a retry that can only fail.
@netlify

netlify Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for vortexfi canceled.

Name Link
🔨 Latest commit 49ad977
🔍 Latest deploy log https://app.netlify.com/projects/vortexfi/deploys/6ac4e29aa7ae4300082255fb

@netlify

netlify Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for vortex-sandbox ready!

Name Link
🔨 Latest commit 49ad977
🔍 Latest deploy log https://app.netlify.com/projects/vortex-sandbox/deploys/6ac4e29a3ee56a0008b84c89
😎 Deploy Preview https://deploy-preview-1401--vortex-sandbox.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for vrtx-dashboard ready!

Name Link
🔨 Latest commit 49ad977
🔍 Latest deploy log https://app.netlify.com/projects/vrtx-dashboard/deploys/6ac4e29a8b3526000843594d
😎 Deploy Preview https://deploy-preview-1401--vrtx-dashboard.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

ebma added 3 commits October 5, 2026 12:32
…ry toast

Nothing listens to TRANSFER_FAILED, and the retry panel already shows the error inline.
…amp-post-broadcast-retry

# Conflicts:
#	apps/dashboard/src/components/transfer/OnrampForm.tsx
#	apps/dashboard/src/components/transfer/TransferForm.tsx
#	apps/dashboard/src/machines/transfer.actors.ts
Since #1394 the recovery worker starts a non-domestic SELL whose source hash
was reported once its start deadline passes, but the expired panel still said
the transfer could no longer start and offered a new one, inviting a second
payment. A start that succeeded before a reload looked the same.
@ebma
ebma merged commit 6939694 into staging Oct 6, 2026
6 checks passed
@ebma
ebma deleted the fix/dashboard-offramp-post-broadcast-retry branch October 6, 2026 12:04
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