Repository navigation
feat(tempo): add Relay.autoSwap, opt-in balance diffs, and match diffs to state - #5356
Conversation
- Added `Relay.autoSwap()`: on an `InsufficientBalance` revert it buys the missing amount on the stablecoin DEX from the first funded token with a route within the slippage bound, and reports `capabilities.autoSwap`. The DEX debits the sender directly, so the swap adds no allowance. - `Relay.simulate` reports same-token incoming and outgoing transfers separately, and approval exposure as the allowance read after the calls for every `Approval` the account emitted, net of what spenders consumed. - Relay token metadata reads standard ERC-20 fields for non-TIP-20 tokens, so one vault share no longer omits the whole preview. - Added a balance-diff oracle that reads balances and allowances before and after the calls in one simulated block, with localnet precompile vectors, mainnet vectors from the most-called contracts, and a node fuzz suite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D3nmWess9BFbYNyQxrugjp
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: 02d3826 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 02cb97350c
ℹ️ 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".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Published pnpm add viem@pr-5356
pnpm add viem@0.0.0-pr-5356-02d3826 |
…ippage - Candidates are quoted first and accepted when the quote fits both the slippage bound and the balance; the DEX spends only the quoted input. - Slippage is floored to parts per million, so the maximum input never exceeds the configured bound. - Added `Capabilities.AutoSwap` to the fill capability schema with a type test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D3nmWess9BFbYNyQxrugjp
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2dfb410d8c
ℹ️ 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".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| decimals: number | ||
| formatted: string | ||
| name: string | ||
| symbol: string | ||
| token: Address |
There was a problem hiding this comment.
Add TSDoc to AutoSwapAmount fields
The newly exported AutoSwapAmount properties have no TSDoc, leaving consumers without generated descriptions for fields such as formatted, token, and value. Add property-level documentation to this public type as required for exported API shapes.
AGENTS.md reference: AGENTS.md:L120-L121
Useful? React with 👍 / 👎.
| #### options.store | ||
|
|
||
| - **Type:** `Store.Store` | ||
|
|
||
| Store for cached token metadata. Omit to read metadata for each request. |
There was a problem hiding this comment.
Demonstrate store in an auto-swap recipe
The page documents options.store, but none of its recipes demonstrate configuring a store or explain when cached metadata is useful. Add a recipe that exercises this option rather than leaving it only in the reference section.
AGENTS.md reference: AGENTS.md:L300-L304
Useful? React with 👍 / 👎.
| .catch(() => { | ||
| throw error |
There was a problem hiding this comment.
Try another swap source after refill failure
When the first ranked input token is also needed by a later original call or for fees, its balance can cover the quote even though prepending the buy makes the refill revert. This catch immediately restores the original error, so a second independently funded and routable candidate is never attempted; for example, a missing-token transfer followed by a transfer of the preferred input token fails even when another candidate could fund the swap. Continue with the remaining candidates after this refill failure, or reserve the original transaction's spend when selecting the source.
Useful? React with 👍 / 👎.
| // Previews over the token budget are omitted, so their reads are skipped too. | ||
| if (pairs.size === 0 || pairs.size > 100) return undefined |
There was a problem hiding this comment.
Omit previews above the allowance-read cap
When one transaction emits more than 100 approval pairs, this returns undefined, but buildBalanceDiffs interprets that exactly like a failed read and falls back to each pair's last emitted approval amount. If any of those allowances was subsequently consumed, the returned preview overstates exposure instead of being omitted; this is especially possible when many spenders share one token, because the later 100-token limit does not reject the result. Propagate an explicit over-budget result and omit the preview rather than using event amounts.
Useful? React with 👍 / 👎.
| // Without both tokens' metadata the swap still executes, but cannot be described. | ||
| if (!metadata_in || !metadata_out) | ||
| return { capabilities: { autoSwap: undefined } } |
There was a problem hiding this comment.
Preserve swap disclosure when metadata reads fail
When either post-fill metadata lookup fails, this patch removes capabilities.autoSwap even though the returned transaction still contains and will execute the prepended DEX call. A transient RPC failure or a token whose metadata cannot be decoded therefore leaves callers with no structured indication that the relay modified the transaction to spend another token. Retain a capability that discloses the known token addresses and bounds, or fail the recovery before returning a silently modified fill.
Useful? React with 👍 / 👎.
| const allowances = | ||
| account && account !== zeroAddress | ||
| ? await readAllowances(client, { account, blockNumber, calls, logs }) | ||
| : undefined |
There was a problem hiding this comment.
Pin allowance replay to the first simulation state
When blockNumber is omitted, the transfer/log simulation and readAllowances each resolve latest independently. If a block is produced between those RPCs, the second invocation replays the calls from a different base state, so its final allowances can disagree with the execution whose logs and transfers are used to build the same preview. Capture a common base block before both simulations, or otherwise replay the allowance reads against the exact state used by the first simulation.
Useful? React with 👍 / 👎.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D3nmWess9BFbYNyQxrugjp
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 451d306802
ℹ️ 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".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| .catch(() => { | ||
| throw error |
There was a problem hiding this comment.
Continue recovery when a later call exposes another deficit
When a batch contains multiple calls spending the missing token, the first revert reports only the first failing call's shortfall. For example, with zero balance and two one-token transfers, the plugin buys one token, the first transfer succeeds, the second then reverts, and this catch discards the retry error and restores the original one. Retry recovery for subsequent deficits, or determine the batch's full deficit before constructing the swap.
Useful? React with 👍 / 👎.
| Largest accepted price slippage over the missing amount, between 0 and 1. | ||
|
|
||
| #### options.store |
There was a problem hiding this comment.
Add focused examples for every documented option
Both new parameter sections stop after their type and prose, so neither options.slippage nor options.store has the required focused ts twoslash example showing realistic use. Add a minimal focused snippet under each option heading.
AGENTS.md reference: AGENTS.md:L336-L343
Useful? React with 👍 / 👎.
- `Relay.simulate` returns `balanceDiffs` only for `capabilities.balanceDiffs:
true`; the fee estimate stays default. Insufficient-balance errors follow
the same opt-in for their optimistic diffs.
- `Relay.autoSwap` swaps only for `capabilities.autoSwap: true` or
`{ slippage }`, which can tighten but never loosen the configured bound.
- Tempo request formatting keeps `capabilities` on fill requests without
Tempo-specific fields, which previously dropped `errors: true` too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3nmWess9BFbYNyQxrugjp
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 553d4d5bc3
ℹ️ 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".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ): { slippage?: number } | undefined { | ||
| const value = (capabilities as Record<string, unknown> | undefined)?.autoSwap | ||
| if (value === true) return {} | ||
| if (!value || typeof value !== 'object') return undefined |
There was a problem hiding this comment.
Reject arrays as auto-swap opt-ins
For raw JSON-RPC requests with capabilities.autoSwap: [], this check treats the array as an options object and returns {}, so the relay may prepend a swap even though the caller did not provide either supported opt-in form (true or an object). Because this capability changes the transaction the sender signs, exclude arrays here and reject them as invalid parameters or treat them as no opt-in.
Useful? React with 👍 / 👎.
| const optimisticCalls = | ||
| normalized && | ||
| (parameters.capabilities as Record<string, unknown> | undefined) | ||
| ?.balanceDiffs === true | ||
| ? extractCalls(normalized) |
There was a problem hiding this comment.
Resolve virtual addresses independently of balance diffs
When an insufficient-balance fill contains a virtual-address call but does not opt into balanceDiffs, optimisticCalls becomes undefined, so the shared ternary also skips resolveVirtualAddresses. The error result bypasses normal post-fill enrichment, causing the otherwise unrelated virtualAddresses capability to disappear unless callers request a balance preview; keep virtual-address resolution based on the normalized calls while gating only the simulation.
Useful? React with 👍 / 👎.
- Reserved 5,000 gas for a fee payer's signature and fee token on sponsored requests: `eth_fillTransaction` estimates without them, and T14 charges them as intrinsic gas. - Gated admin access-key tests on hardforks before T14, which removes admin key creation (TIP-1137); test relays mirror `Relay.feePayer`'s gas reserve. - Overrode follow-redirects to 1.16.2 and ignored three dev-only moderate advisories without usable patches, so `pnpm audit` passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D3nmWess9BFbYNyQxrugjp
Owner approvals cover the gas limit, so re-preparing an approved request for broadcast must not add the fee payer's gas reserve again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D3nmWess9BFbYNyQxrugjp
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 02d382600d
ℹ️ 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".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // The fill omits the fee payer's signature and fee token (85 bytes), which count toward intrinsic gas. | ||
| request.gas = (request.gas ?? 0n) + feePayerGas | ||
| if (request.keyAuthorization?.signature.type === 'webAuthn') | ||
| request.gas = (request.gas ?? 0n) + 20_000n |
There was a problem hiding this comment.
Avoid adding the fee-payer reserve on every preparation
When an unsigned sponsored request is passed through prepareTransactionRequest more than once, each afterFillParameters pass adds another 5,000 gas because the returned request still has feePayer, no feePayerSignature, and no owner approvals. For example, re-preparing the { gas: 38_000n, feePayer: true } output of the new ordinary-account test produces 43_000n; repeated preparation can therefore inflate the signed gas limit and make fee-balance checks reject an otherwise valid transaction. Track whether the reserve is already included or derive the gas from an unadjusted estimate.
Useful? React with 👍 / 👎.
| tokenOut: deficit.token, | ||
| }) | ||
| const swap = [{ data: buy.data, to: buy.to, value: '0x0' }] | ||
| const { calls: _, data: __, to: ___, value: ____, ...rest } = parameters |
There was a problem hiding this comment.
Re-estimate gas after prepending the swap
When the original fill request already supplies gas, this retry keeps that value in rest even though it prepends a DEX call. Since eth_fillTransaction fills missing fields rather than replacing the caller's explicit gas limit, the returned transaction can retain a limit sized only for the original calls and then run out of gas when executing the added swap. Omit gas from the retry so the modified call list is estimated again.
Useful? React with 👍 / 👎.
| tx: | ||
| Array.isArray(tx.calls) && tx.calls.length > 0 | ||
| ? tx | ||
| : { ...tx, calls }, |
There was a problem hiding this comment.
Propagate swapped calls through transaction preparation
When auto-swap is reached through prepareTransactionRequest (including the documented recipe and normal send preparation), fillTransaction receives this modified tx.calls, but prepareTransactionRequest only copies selected filled fields such as gas, fees, and nonce back onto the original request and therefore retains the unswapped calls. The returned request can advertise capabilities.autoSwap while the transaction that is subsequently signed omits the buy and still fails for insufficient balance; make the preparation path adopt the filled call list.
Useful? React with 👍 / 👎.
Adds
Relay.autoSwap()and fixes threeRelay.simulatepreview gaps found while building it. Balance diffs and auto swaps now require a per-request opt-in. All three gaps are covered by an oracle-backed vector and fuzz suite.Request opt-in
capabilities.balanceDiffs: trueopts intobalanceDiffs. Previously diffs were on by default; the fee estimate still is. Insufficient-balance errors include their optimistic diffs only with this opt-in.capabilities.autoSwap: true | { slippage }opts into a swap. A requestedslippagecan only tighten the plugin's configured bound, never loosen it. Without the opt-in, the original revert is returned unchanged.to/data) through the generic formatter, which droppedcapabilities, includingerrors: true. It now keeps them.Migration: relays and wallets that render previews now send
capabilities: { balanceDiffs: true }.Relay.autoSwap()When an opted-in fill reverts with
InsufficientBalance, the plugin buys the missing amount on the stablecoin DEX, prepends the buy to the user's calls, refills throughcontext.client, and reports the swap ascapabilities.autoSwap. Token metadata for the capability is added inafterFill.Relay.simulate(), so it sees the node's revert beforesimulateturns it into an error capability.Simulate fixes
Approvalamount, which overstated exposure after a router spent part of it. Now the remaining allowance is read after the calls for everyApprovalthe sender emits, however the approval was made (direct call, router, permit).decimals/symbol/name.Tests
test/src/tempo/balanceDiffs.tsadds an oracle that is independent of how diffs are computed. In one simulated block it reads the sender's balances and allowances immediately before and after the calls, then checks the diffs against those reads and against the emittedTransferevents.VITE_TEMPO_CREDENTIALS, because the public RPC refusestempo_simulateV1.test:fuzz:tempo:node).Capabilities.AutoSwap.Against
main'ssimulate.ts: the localnet vectors fail 3, the mainnet vectors fail 4, and the fuzz suite fails with a counterexample shrunk to[buy, sell, liquidity].Locally, the relay and compat suites have 348 passed and 1 failed. The failure is "does not save key authorizations signed by an admin key" in
keyAuthorization.test.ts, which also fails onmainand is unrelated.Unrelated CI failures
These also fail on
main's latest run:pnpm auditadvisories.InvalidKeyId) and afeePayerintrinsic-gas test.Not handled
When a fill goes to an external fee-payer URL and that relay reports
InsufficientBalance, the error stays insidefeePayer, so autoSwap can't recover it. That relay should runRelay.autoSwap()itself.🤖 Generated with Claude Code
https://claude.ai/code/session_01D3nmWess9BFbYNyQxrugjp