Skip to content

Gasless USDC/USDT0 transfers (replaces OpenGSN for transfers) - #317

Draft
sisou wants to merge 3 commits into
masterfrom
soeren/gasless-transfers
Draft

sisou wants to merge 3 commits into
masterfrom
soeren/gasless-transfers

Conversation

@sisou

@sisou sisou commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

Stablecoin sends (the send modal and Moonpay sells) now use gasless USDC/USDT0 transfers through the GaslessTransfer contract and Nimiq relay of NimiqToolbox/gas-abstraction (private) instead of OpenGSN. Mainnet has been in gas abstraction maintenance mode since 2026-09-18. Swaps, HTLC refunds and the USDC.e conversion are unchanged and still use OpenGSN; they move to the new gas abstraction in a follow-up.

Part of a coordinated change

Repo PR Depends on
Keyguard nimiq/keyguard#570 –
Hub nimiq/hub#592 nimiq/keyguard#570
Wallet this PR nimiq/hub#592 (new @nimiq/hub-api types)

Release order: Keyguard, then Hub, then Wallet.

Flow

  1. The relay quotes the fee (GET /v1/fee, permit mode). The Wallet refuses quotes above maxAcceptableFee (0.50); the Keyguard applies the same limit.
  2. The Wallet reads the sender's permit nonce and checks the token's DOMAIN_SEPARATOR() against the pins.
  3. The Hub and Keyguard sign the intent and a permit (signPolygonTransaction with a SignPolygonGaslessTransferRequest).
  4. The Wallet records the signed version, then submits it (POST /v1/transfer), re-posting after unclear answers. It waits until the transfer is mined and parses the receipt's TransferRelayed event.

Never paying twice

Every version of a payment keeps its intent nonce, so at most one executes (see the SDK's "Failures, retries and replacements").

  • stores/GaslessPayments.ts records each payment's latest signed version before it is submitted, until its outcome is final at the finalized block. It keeps no signatures. It is persisted, and on startup the outcome checks resume.
  • A retry, or a new send of the same token to the same recipient after a failure (also after a reload, or from Moonpay), is a correction that keeps the nonce. Before signing a correction, the Wallet checks on-chain whether a version already executed.
  • lib/usdc/GaslessOutcome.ts is an ethers v5 port of the SDK's confirmOnChain / waitForTransferOutcome. The Wallet doesn't use viem.

Other changes

  • History: a relay fee is recognized in gasless transactions and merged into the principal transfer. OpenGSN fee detection stays for past transactions. OpenGSN contracts that are not configured are skipped, which fixes the history sync on the local stack.
  • Config: polygon.gasless (relay URL, contract, relay addresses, deploy block, fee limits). Sending is controlled by the new isGaslessTransferUnderMaintenance; isGasAbstractionUnderMaintenance still controls swaps. The local config targets the gas-abstraction Anvil stack.
  • Removed: only createTransactionRequest, the OpenGSN transfer request.
  • Fixed: Moonpay USDC sells used the USDT token contract.

Before merging

  • Set polygon.gasless.relayUrl and relays in config.mainnet.ts (TODOs), then set isGaslessTransferUnderMaintenance: false.
  • Replace "@nimiq/gasless-sdk": "file:../gas-abstraction/sdk" with the published package. A clean install fails without the sibling checkout.
  • Update @nimiq/hub-api to the version released from Gasless USDC/USDT0 transfers (replaces OpenGSN for transfers) hub#592. Locally, this was tested with yarn link.
  • End-to-end test against the local Anvil stack (send, fee_too_low and sender_busy corrections, relay restart, reload during a pending payment), then a mainnet canary from team accounts.

Open points

  • A send is lowered to balance − fee if needed, as before. A Moonpay deposit would then arrive short; refusing may be better.
  • waitForTransferOutcome has no per-request time budget like the SDK, so a hung RPC can stall that background check.

Testing

  • tsc (the only src/ errors are the two in router.ts that also exist on master), yarn lint, testnet build.
  • Not tested end to end yet.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AKgXoeSB62akioiTzYya6C

sisou and others added 3 commits September 30, 2026 18:09
Stablecoin sends (including Moonpay sells) now use the GaslessTransfer
contract and Nimiq relay of NimiqToolbox/gas-abstraction instead of
OpenGSN: the relay quotes the fee, the Keyguard signs the transfer intent
and a permit via the Hub, and the relay submits it and pays the POL gas.

- lib/usdc/Gasless.ts: pins, relay client (@nimiq/gasless-sdk), fee
  quotes, permit nonce and token domain check, submission with reposts,
  and corrections that keep a failed payment's nonce.
- lib/usdc/GaslessOutcome.ts: ethers v5 port of the SDK's on-chain
  outcome check (confirmOnChain, waitForTransferOutcome).
- stores/GaslessPayments.ts: records each payment's latest signed version
  before submitting it, until its outcome is final at the finalized
  block, so that the user never pays twice; resumed on startup.
- History: recognize the relay fee of gasless transfers; OpenGSN fee
  detection is kept for past transactions.
- Remove OpenGSN relay selection, fee calculation and relaying, and with
  it USDC/USDT swaps, HTLC refunds and the USDC.e conversion, which have
  no gasless replacement yet. Historic swaps are still displayed.
- Rename isGasAbstractionUnderMaintenance to
  isGaslessTransferUnderMaintenance; mainnet stays in maintenance until
  the relay address and URL are configured.
- The local config targets the gas-abstraction Anvil stack.

@nimiq/gasless-sdk is referenced as a local file dependency until it is
published.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AKgXoeSB62akioiTzYya6C
- A send of the same token to the same recipient as a payment that failed
  (as far as the user was told) is a correction of it, also after a reload
  or from another flow such as Moonpay, so that at most one executes.
- Keep a failed payment for the retry if the user cancels signing, and
  report an earlier version that executed after all instead of sending
  again.
- Skip the legacy allowance reads of unconfigured OpenGSN contracts, which
  broke the history sync on local and testnet.
- Catch fee quote errors in the network overview.
- Moonpay USDC sells used the USDT token contract.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AKgXoeSB62akioiTzYya6C
Swaps will move to the new gas abstraction separately, so restore the
OpenGSN-based swap, HTLC refund and USDC.e conversion code, their config
and the @opengsn/common dependency. Only plain transfers use the gasless
relay:

- Remove only createTransactionRequest, the OpenGSN transfer request.
- isGasAbstractionUnderMaintenance keeps controlling stablecoin swaps;
  sending is controlled by isGaslessTransferUnderMaintenance.
- History sync skips OpenGSN contracts that are not configured, as on the
  local Anvil stack.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AKgXoeSB62akioiTzYya6C

This branch has not been deployed

No deployments
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