Skip to content

feat(approval): migrate EVM and Tron approval to the prerequisites flow - #1442

Open
arlert-armin wants to merge 5 commits into
nextfrom
feat/evm-approval-prerequisites
Open

arlert-armin wants to merge 5 commits into
nextfrom
feat/evm-approval-prerequisites

Conversation

@arlert-armin

Copy link
Copy Markdown
Contributor

Summary

Migrates EVM and Tron token approvals from the legacy flow (isApprovalTx on the create-transaction response + polling the server check-approval endpoint) to the unified prerequisites pattern already used for XRPL/Stellar trustlines. When the client opts in (validations.approve: false), the backend returns the approval requirement in the transaction's prerequisites list, and the client checks the on-chain allowance, builds/signs the approve itself, and waits for confirmation through the wallet's namespace — no server round-trips for approval.

Key decisions

  • Per-wallet opt-in, fully backward compatible. A wallet uses the new flow only if its hub namespace exposes the approval read action. Everything else — WalletConnect (no hub namespace) and any not-yet-migrated wallet — stays on the untouched legacy flow. No wallet is forced onto the new path.
  • One generic action, per-chain adapters. EVM and Tron share a single checkApprove / checkApproveTransactionStatus pair; each chain supplies a small adapter (build-tx, status-check, namespace key). Avoids duplicating ~85% of the logic and lets other chains slot in later.
  • EVM: injected wallets read via their EIP-1193 provider; Ledger/Trezor (no injected provider) read via a JSON-RPC instance shared with their existing signer.
  • Tron: the approve tx is built client-side via the wallet's TronWeb (the Tron signer needs a fully-built tx); supported on bitget and tron-link.

Dependent PRs

This branch pins experimental builds of two dependencies while their PRs are in review. Both must merge and publish stable versions before this can:

Note: the rango-types / @hub3js/evm pins in this diff are the experimental versions; they will be swapped for the stable releases once the two PRs above land.

How did you test this change?

  • EVM swap needing approval on an injected wallet (Phantom) — allowance-skip + full approve path
  • Tron swap needing approval on bitget / tron-link
  • WalletConnect / not-yet-migrated wallet — still uses the legacy approval flow
  • build:widget green

Checklist:

  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • I have added tests that prove my fix is effective or that my feature works
  • Implemented a user interface (UI) change, referencing our Figma design to ensure pixel-perfect precision.

@RyukTheCoder RyukTheCoder 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.

Tnx, just added some comments.

Comment thread queue-manager/rango-preset/src/actions/checkPrerequisites/utils.ts
Comment thread queue-manager/rango-preset/src/actions/checkApprove/checkApprove.ts Outdated
Comment thread queue-manager/rango-preset/src/actions/checkApprove/checkApprove.ts
Comment thread queue-manager/rango-preset/src/actions/checkApprove/utils.ts Outdated
Comment thread queue-manager/rango-preset/src/actions/checkApprove/types.ts Outdated
Comment thread widget/app/src/App.tsx
Comment thread wallets/provider-ledger/src/signers/ethereum.ts Outdated
Comment thread wallets/provider-ledger/src/utils.ts Outdated
Comment thread wallets/core/src/namespaces/tron/types.ts Outdated
Comment thread wallets/core/src/namespaces/tron/utils.ts Outdated
@arlert-armin
arlert-armin force-pushed the feat/evm-approval-prerequisites branch 3 times, most recently from 09612d9 to 62e9de0 Compare July 20, 2026 10:05
Comment thread wallets/provider-trezor/src/utils.ts Outdated
Comment thread queue-manager/rango-preset/src/actions/checkApprove/utils.ts Outdated
Comment thread queue-manager/rango-preset/src/actions/checkApprove/types.ts Outdated
Comment thread queue-manager/rango-preset/src/actions/createTransaction.ts Outdated
Comment thread queue-manager/rango-preset/src/actions/createTransaction.ts Outdated

@RyukTheCoder RyukTheCoder 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.

LGTM

@arlert-armin
arlert-armin force-pushed the feat/evm-approval-prerequisites branch from 5de39de to 57d147c Compare September 8, 2026 12:57
@arlert-armin
arlert-armin force-pushed the feat/evm-approval-prerequisites branch from 57d147c to dfb4bac Compare September 8, 2026 13:16
@arlert-armin
arlert-armin force-pushed the feat/evm-approval-prerequisites branch from dfb4bac to 539c844 Compare September 8, 2026 14:07
@arlert-armin
arlert-armin force-pushed the feat/evm-approval-prerequisites branch from 539c844 to 6c2441a Compare September 9, 2026 13:39
Approving makes the user wait twice and the two waits are not the same: a
prompt is sitting in their wallet during the first, and nothing is asked of
them during the second. `StepStatus` has no separate signing state, so both
stay on `waitingForApproval` and are told apart by their message and event.
The message has to carry it, because the swap-details alert renders
`extraMessage` and falls back to `extraMessageDetail` only for a failed step.

`isApprovalCurrentStepTx` reads the legacy `*ApprovalTransaction` step fields,
which this flow never sets, so approval events rendered the generic swap copy.
Rather than widen that predicate - it also selects state transitions in
`produceNextStateForTransaction` - the notifier takes an optional `isApproval`
flag and falls back to the existing check.

The status poller's catch was bare, so any failure there became an indefinite
silent retry indistinguishable from a transaction still being pending. It now
reports what it caught before retrying.
Registers `getAllowance` and `getTransactionReceipt` on the WalletConnect EVM
namespace so approvals resolve through the prerequisites flow like the other
EVM providers.

Both reads are implemented on the adapter, which already owns the universal
provider, rather than reaching for an EVM instance from the namespace - that
keeps the namespace actions as thin delegations and avoids casting the
WalletConnect provider to `ProviderAPI`.

They are served by the chain's node rather than the wallet.
`UniversalProvider.request` throws unless `universalProvider.connect()`
established the session, and this adapter drives the SignClient directly, so
it has no session of its own and every call failed before reaching the
network. Routing them to the wallet is not possible either: a session only
authorizes the methods it negotiated, and those are signing-only - `eth_call`
is not part of the eip155 RPC reference. WalletConnect's own provider makes
the same split.

Providers are memoized per chain, because the endpoint has to follow the
session's chain: a token address is meaningless on the wrong chain, and an
`eth_call` against it answers `0x`, which would read back as a zero allowance
and re-approve on every attempt.
Tron approvals sat on "waiting for approval" indefinitely, for three reasons.

A wallet that lets the user choose the allowance does not sign the transaction
it was handed: it rebuilds the call with the amount the user entered, reading
it from `__payload__`. Server-built transactions carry that field,
`transactionBuilder` does not return it, and ours was hardcoded to `{}` - so
TronLink had nothing to rebuild from, reported "Modification failed", and the
approve could never be signed. Wallets that decode `raw_data` themselves were
unaffected, which is why it looked like an inconsistency between providers.
The payload is derived from the transaction the node returned, so it cannot
describe a call other than the one that was built.

A refused broadcast resolves rather than throwing, and still carries a `txid`,
since the id comes from the transaction's own bytes. Taking it at face value
left the queue polling a transaction no node ever had. Refusals arrive in more
than one shape and never set `result` to `false` - it is simply absent - so
both `code` with a hex `message` and a capitalised node-side `Error` are
treated as failures, and the reason is surfaced instead.

Status was read with `getTransactionInfo`, which queries the solidity node and
answers nothing until the transaction solidifies, roughly 19 blocks behind the
head. The full-node lookup answers as soon as it is in a block, about 3s. The
result is also confirmed on `blockNumber`, because Tron sets `receipt.result`
to a failure code only on failure and may omit it on success.

OKX was the only wallet with a Tron namespace that registered no approve
actions, so its Tron swaps stayed on the server-driven flow.
Both devices sign a raw transaction on-device and work nothing out for
themselves, so they refused anything without a gas limit, and Trezor refused
anything without pricing. Server-built transactions carry both, but the
approve prerequisite is built on the client and leaves every gas field null,
so approving through either wallet failed outright.

The limit is estimated and the pricing quoted through the provider the signer
already creates, and only where the transaction supplied neither. Anything it
does supply is preferred and passed through untouched, so a server-built
transaction is still signed with exactly the gas it was created with and costs
no extra call.

The node's quote settles on one scheme rather than both, since a signed
transaction cannot carry them together: EIP-1559 wherever the chain quotes it,
and a legacy price otherwise.
The status poller advanced on a single receipt plus a single allowance read,
both answered by the wallet's RPC - which is a load balancer over several
nodes. One read agreeing with the receipt is not evidence that the node the
swap is submitted to has applied the approve block, so the swap went out
against state where the allowance was still missing and reverted, while a
manual retry moments later succeeded.

The allowance now has to read as sufficient on consecutive reads a few seconds
apart before the prerequisite is met, and a short read discards the reads
before it rather than being believed. It is the allowance, not the receipt,
that the swap depends on.

The receipt status was also compared against the exact string `0x1`, so a
wallet handing it over as `0x01` or as a number would have reported a
successful approval as failed.
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