Skip to content

Wait for 0x Gasless swaps to mine before saving - #513

Merged
j0ntz merged 1 commit into
masterfrom
jon/0x-gasless-mined-hash
Nov 14, 2026
Merged

j0ntz merged 1 commit into
masterfrom
jon/0x-gasless-mined-hash

Conversation

@j0ntz

@j0ntz j0ntz commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

none

Description

Asana task

0x Gasless saved the swap under whatever hash the status endpoint listed first. The old poll loop (status === 'pending' || transactions.length !== 1) exited on submitted with one hash. Per the 0x getStatus docs, submitted can list several transactions and only one of them mines. If the relayer replaced the hash, the wallet kept a row under a hash that never mines. That row stays at blockHeight === 0, and EthereumEngine.makeSpend throws PendingFundsError on every later send. The same loop also spun forever on failed, which can report zero transactions.

Changes in waitForMinedTxid:

  • Polls through pending and submitted. It returns only on succeeded or confirmed, the statuses where 0x reports the mined transaction.
  • Throws on failed with the 0x reason.
  • Retries a failed status request with a doubling delay (capped at 16 s) until the deadline. 0x already holds the signed trade at this point and may still mine it. One failed request no longer ends the swap.
  • Returns a mined hash only after checking it is a well-formed 32-byte hash, and throws if a mined status lists more than one transaction.
  • Stops after 5 minutes with an error saying the swap may still complete.
  • Logs each status change with its hashes at warn level, because the app runs core at warn and lower levels never reach a log export.

The swap's orderId, txid and saved transaction now all use the mined hash.

Added wait: one poll cycle (500 ms) plus block inclusion. On Arc the first poll already returned succeeded, about 1.1 s after the swap started. On Ethereum mainnet expect about one block (~12 s). The existing swap-processing state covers this wait. The gui needs no change.

Tests: unit tests for the same-hash, replaced-hash, confirmed, failed, timeout, failed-status-request, malformed-hash and several-transactions cases, plus an approve() test that the saved transaction uses the mined hash. The approve test fails against the old loop, which saves the submitted hash.

Real swap on the iOS sim at the current head: 0.4525 EURC to 0.484 USDC on Arc via 0x Gasless, through to the success scene. The plugin logged 0x gasless status: submitted [0x6e6ed4ee...7e40], then 0x gasless status: succeeded with the same hash. Core saved that hash as both the txid and the orderId. On chain that transaction mined in block 30944708 with status 1, sent by the 0x relayer. The earlier swap on the first commit (0.445 EURC, block 26367212) behaved the same way. Neither swap listed a replaced hash; the unit test covers that case.

Timeout error on the sim (hack-forced, reverted, never committed): I stubbed the 0x submit so no funds moved, pinned the status to submitted, and cut the timeout to 5 s. The swap scene shows the error as an alert ("Swap timed out after 5 seconds waiting for 0x to mine it (last status: submitted [...]). It may still complete."), and the slider re-arms.

Existing stuck rows are not cleared by this change. accb's addTransaction stamps lastSeenTime on these rows, and the one-day drop rule can mark them dropped. That does not unblock sends: getUnconfirmedTxs returns every row at blockHeight 0, dropped or not, and makeSpend throws PendingFundsError for any of them. Wallets that already have a stuck row need a separate cleanup that removes it.


Note

Medium Risk
Changes when and which hash is written to the wallet after gasless swaps; incorrect behavior previously caused stuck pending txs, but approve now blocks longer until mining or timeout.

Overview
Fixes 0x Gasless swaps that were persisted under a submitted transaction hash the relayer could replace, leaving a never-mining pending row that blocked later sends.

After submit, the plugin now uses waitForMinedTxid to poll status (500 ms interval, 5 minute cap) until 0x reports succeeded or confirmed, then sets orderId, txid, and the saved wallet transaction to that mined hash. failed surfaces the API reason; timeouts error with a message that the swap may still complete. Status transitions and hashes are logged on change.

Adds unit tests for replaced-hash, failure, timeout, and approve() saving the mined hash.

Reviewed by Cursor Bugbot for commit c51378c. Bugbot is set up for automated code reviews on this repo. Configure here.

Test evidence

c51378c
Wait for 0x Gasless swaps to mine before saving
2026-10-17
🪓 stubbed the 0x submit (no funds moved), pinned the status to submitted, and cut the timeout to 5 s

1. arc eurc usdc 0x gasless quote

2. swap success

3. 🪓 timeout error
4c7f0bf
fixup! Wait for 0x Gasless swaps to mine before saving
2026-11-13

11. gasless quote eurc to usdc
0x Gasless quote, 0.4525 EURC to 0.484 USDC on Arc, at fixup 4c7f0bf

12. gasless swap success
Success scene after the poll saw succeeded; saved txid 0x6e6ed4ee...7e40 mined in block 30944708

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-17T03:30:44.678963Z c51378c PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@peachbits peachbits left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0x's getStatus reference says a submitted trade can list several transactions, of which one mines, and that succeeded (in a block) and confirmed (three confirmations) return only the mined one, so waiting for those two saves the hash that mines. Locally tsc and eslint are clean and the full suite passes (276). With the old exit test (any non-pending status that lists one transaction) put back, 4 of the 5 new tests fail, the approve test among them.

Blocking: one failed status request still ends a swap that 0x already has, and two of the loop's exits have no test (inline).

Non-blocking: the body says gasless rows are saved without lastSeenTime, so the one-day drop never fires for them. accb's addTransaction stamps lastSeenTime on every unconfirmed row that saveTx adds. The drop wouldn't unblock sends anyway: getUnconfirmedTxs returns every row at blockHeight 0, dropped or not, and makeSpend throws PendingFundsError for any of them. Wallets with a stuck row still need a cleanup, and marking the row dropped won't be enough.

Comment thread src/swap/defi/0x/0xGasless.ts Resolved
Comment thread test/zeroXGasless.test.ts Resolved
Comment thread src/swap/defi/0x/0xGasless.ts Resolved
Comment thread src/swap/defi/0x/0xGasless.ts Resolved
@j0ntz

j0ntz commented Nov 13, 2026

Copy link
Copy Markdown
Contributor Author

Both blocking items are fixed in 4c7f0bf (inline replies). The PR body no longer claims gasless rows lack lastSeenTime: it now says addTransaction stamps it, that a dropped row still blocks sends through getUnconfirmedTxs and PendingFundsError, and that wallets with a stuck row need a separate cleanup.

@peachbits peachbits left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All four points from the last review are fixed in 4c7f0bf. Checked locally:

  • A failed status request no longer ends the swap. The retry test fails with the try removed, and approve() now saves under the mined hash after one 503 or one dropped request. A status request that always fails stops at the deadline (23 requests, the wait doubling to 16 s), and approve() then rejects with the timeout error and saves nothing. A failed answer after failed requests still throws its reason at once.
  • The confirmed and several-hashes tests fail without their branches, the hash test fails with the check removed or loosened, and both log lines use log.warn.
  • The stuck-row paragraph in the PR body now matches accb.
  • tsc and eslint are clean, and the full suite passes (280).

Non-blocking: autosquash drops the fixup's message, so the final commit keeps its 10-17 body, which names the failed throw, the five-minute bound and the status log but not the retry or the hash check. Your fixup body already explains both; an amend! that adds them (or a reword at land) keeps that in history.

@j0ntz
j0ntz force-pushed the jon/0x-gasless-mined-hash branch from 4c7f0bf to b8191d6 Compare November 14, 2026 00:29
@cursor

cursor Bot commented Nov 14, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@j0ntz

j0ntz commented Nov 14, 2026

Copy link
Copy Markdown
Contributor Author

Took the commit-message suggestion: the branch is now one commit, b8191d6, and its message names the retry with backoff and the single-hash check alongside the original failed-status and five-minute bound. The code tree is unchanged from 4c7f0bf.

The status poll exited on 'submitted' with one transaction and saved
that hash. The relayer can replace a submitted transaction, so the
saved row never confirmed and the wallet blocked later sends as
pending. Poll until 'succeeded' or 'confirmed' and save the hash 0x
reports as mined. A mined status must carry exactly one well-formed
32-byte hash; several hashes or a malformed one throw instead of
saving a guess.

The loop also spun forever on a 'failed' status with no transactions.
It now throws the failure reason, bounds the wait at five minutes, and
logs each status change at warn. A failed status request no longer
ends the wait: 0x may still mine the signed trade, so the poll retries
with a doubling delay (capped at 16 seconds) until the deadline.
@j0ntz
j0ntz force-pushed the jon/0x-gasless-mined-hash branch from b8191d6 to 30f8873 Compare November 14, 2026 00:33
@cursor

cursor Bot commented Nov 14, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@j0ntz
j0ntz enabled auto-merge November 14, 2026 00:33
@j0ntz
j0ntz merged commit 3d2df84 into master Nov 14, 2026
2 checks passed
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.

2 participants