From 7e92a2af506ed42368e7a6db9d71d2996896aca7 Mon Sep 17 00:00:00 2001 From: jxom Date: Mon, 13 Apr 2026 16:40:45 +1000 Subject: [PATCH 1/3] docs: add Handler.relay and eth_fillTransaction documentation Amp-Thread-ID: https://ampcode.com/threads/T-019d856e-ab9f-74a3-bc14-5e5971998170 --- .../accounts/rpc/eth_fillTransaction.mdx | 297 +++++++++- .../accounts/server/handler.feePayer.mdx | 20 + src/pages/accounts/server/handler.relay.mdx | 549 ++++++++++++++++++ vocs.config.ts | 7 +- 4 files changed, 856 insertions(+), 17 deletions(-) create mode 100644 src/pages/accounts/server/handler.relay.mdx diff --git a/src/pages/accounts/rpc/eth_fillTransaction.mdx b/src/pages/accounts/rpc/eth_fillTransaction.mdx index 8fb027c4..a3ebff09 100644 --- a/src/pages/accounts/rpc/eth_fillTransaction.mdx +++ b/src/pages/accounts/rpc/eth_fillTransaction.mdx @@ -1,18 +1,54 @@ --- title: eth_fillTransaction -description: Fill missing transaction fields like gas and nonce via the node. +description: Fills missing transaction fields and returns wallet-aware metadata. --- # `eth_fillTransaction` -Fills in missing transaction fields (gas, nonce, fees) by querying the node. This is a node RPC method, not handled by the accounts provider directly. +Fills missing transaction fields (gas, nonce, fees) via the node, with wallet-aware enrichment — fee token resolution, simulation-based balance diffs, conditional sponsorship, and automatic AMM resolution for insufficient balances. + +When sent through [`Handler.relay`](/accounts/server/handler.relay), the response is enriched with a `capabilities` object containing balance diffs, fee estimates, sponsor info, and swap details. ## Request ```ts type Request = { method: 'eth_fillTransaction' - params: [TransactionRequest] + params: [{ + /** Access list entries. */ + accessList?: { address: `0x${string}`; storageKeys: `0x${string}`[] }[] + /** Batch of calls. */ + calls?: { + /** Calldata. */ + data?: `0x${string}` + /** Recipient. */ + to?: `0x${string}` + /** Value to transfer. */ + value?: `0x${string}` + }[] + /** Chain ID. */ + chainId?: `0x${string}` + /** Fee token to use. If omitted, the relay picks the user's best token. */ + feeToken?: `0x${string}` + /** Sender address. */ + from?: `0x${string}` + /** Gas limit. */ + gas?: `0x${string}` + /** Key authorization for access key transactions. */ + keyAuthorization?: KeyAuthorization + /** Max fee per gas. */ + maxFeePerGas?: `0x${string}` + /** Max priority fee per gas. */ + maxPriorityFeePerGas?: `0x${string}` + /** Nonce. */ + nonce?: `0x${string}` + /** Nonce key. */ + nonceKey?: `0x${string}` + /** Valid after timestamp. */ + validAfter?: number + /** Valid before timestamp. */ + validBefore?: number + }] } ``` @@ -20,28 +56,259 @@ type Request = { ```ts type Response = { - raw: Hex - tx: TransactionRequest + /** Wallet-specific capabilities computed during fill. */ + capabilities: { + /** AMM swap injected to cover an insufficient balance. */ + autoSwap?: { + /** Max input amount with slippage applied. */ + maxIn: SwapAmount + /** Deficit amount that triggered the swap. */ + minOut: SwapAmount + /** Slippage tolerance (e.g. 0.05 = 5%). */ + slippage: number + } + + /** Per-account balance diffs from simulation (swap-related diffs excluded). */ + balanceDiffs?: { + [account: `0x${string}`]: BalanceDiff[] + } + + /** Fee estimate for the transaction. */ + fee?: { + /** Raw fee amount in token units. */ + amount: `0x${string}` + /** Token decimals (e.g. 6). */ + decimals: number + /** Human-readable fee (e.g. "0.028022"). */ + formatted: string + /** Token symbol (e.g. "AlphaUSD"). */ + symbol: string + } + + /** Sponsor details, present when `sponsored` is `true`. */ + sponsor?: { + /** Sponsor address. */ + address: `0x${string}` + /** Sponsor display name. */ + name?: string + /** Sponsor URL. */ + url?: string + } + + /** Whether the transaction is sponsored by a fee payer. */ + sponsored: boolean + } + /** Fully filled transaction. */ + tx: Record +} + +type BalanceDiff = { + /** Token address. */ + address: `0x${string}` + /** Token decimals (e.g. 6). */ + decimals: number + /** Direction relative to the user. */ + direction: 'incoming' | 'outgoing' + /** Human-readable formatted amount (e.g. "100.00"). */ + formatted: string + /** Token name (e.g. "USDC.e"). */ + name: string + /** Addresses receiving this asset. */ + recipients: readonly `0x${string}`[] + /** Token symbol (e.g. "USDC.e"). */ + symbol: string + /** Token amount. */ + value: `0x${string}` +} + +type SwapAmount = { + /** Token decimals. */ + decimals: number + /** Human-readable formatted amount. */ + formatted: string + /** Token name (e.g. "AlphaUSD"). */ + name: string + /** Token symbol (e.g. "AlphaUSD"). */ + symbol: string + /** Token address. */ + token: `0x${string}` + /** Amount. */ + value: `0x${string}` } ``` ## Example -```ts -import { createPublicClient, http } from 'viem' -import { tempo } from 'viem/chains' +```ts twoslash +import { Provider } from 'accounts' + +const provider = Provider.create() + +const [account] = await provider.request({ + method: 'eth_accounts', +}) + +// ---cut--- +const result = await provider.request({ + method: 'eth_fillTransaction', + params: [{ + from: account, + calls: [{ + to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e + // transfer 100 USDC.e + data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', + }], + }], +}) + +result.capabilities +// { +// balanceDiffs: { +// '0x1234567890abcdef1234567890abcdef12345678': [{ +// address: '0x20c000000000000000000000b9537d11c60e8b50', +// decimals: 6, +// direction: 'outgoing', +// formatted: '100.000000', +// name: 'USDC.e', +// recipients: ['0xcafebabecafebabecafebabecafebabecafebabe'], +// symbol: 'USDC.e', +// value: '0x5f5e100', +// }], +// }, +// fee: { +// amount: '0x6b86', +// decimals: 6, +// formatted: '0.027526', +// symbol: 'pathUSD', +// }, +// sponsored: false, +// } +``` + +### With Fee Token -const client = createPublicClient({ - chain: tempo, - transport: http(), +Pay fees with a specific stablecoin. + +```ts twoslash +import { Provider } from 'accounts' +const provider = Provider.create() +const [account] = await provider.request({ method: 'eth_accounts' }) +// ---cut--- +const result = await provider.request({ + method: 'eth_fillTransaction', + params: [{ + from: account, + calls: [{ + to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e + // transfer 100 USDC.e + data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', + }], + feeToken: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e + }], }) -const filled = await client.request({ +result.capabilities.fee +// { +// amount: '0x6b86', +// decimals: 6, +// formatted: '0.027526', +// symbol: 'USDC.e', +// } +``` + +### With Sponsorship + +When the relay is configured with a [`feePayer`](/accounts/server/handler.relay#feepayer) and the request is sponsorable, the response includes `sponsored: true` and `sponsor` details. + +```ts twoslash +import { Provider } from 'accounts' +const provider = Provider.create() +const [account] = await provider.request({ method: 'eth_accounts' }) +// ---cut--- +const result = await provider.request({ method: 'eth_fillTransaction', params: [{ - from: '0x...', - to: '0x...', - value: '0x0', + from: account, + calls: [{ + to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e + // transfer 100 USDC.e + data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', + }], }], }) + +result.capabilities.sponsored +// true + +result.capabilities.sponsor +// { +// address: '0x1234567890abcdef1234567890abcdef12345678', +// name: 'My App', +// url: 'https://myapp.com', +// } +``` + +### AMM Resolution + +When the user has insufficient balance of a required token, the relay automatically injects swap calls (approve + buy) via the [Stablecoin DEX](/guide/stablecoin-dex). The swap details are reported in `meta.autoSwap`, and swap-related balance diffs are excluded from `meta.balanceDiffs`. + +```ts twoslash +import { Provider } from 'accounts' +const provider = Provider.create() +const [account] = await provider.request({ method: 'eth_accounts' }) +// ---cut--- +const result = await provider.request({ + method: 'eth_fillTransaction', + params: [{ + from: account, + calls: [{ + to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e + // transfer 100 USDC.e + data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', + }], + }], +}) + +result.capabilities.balanceDiffs +// { +// '0x1234567890abcdef1234567890abcdef12345678': [{ +// address: '0x20c0000000000000000000000000000000000001', +// decimals: 6, +// direction: 'outgoing', +// formatted: '100.000000', +// name: 'AlphaUSD', +// recipients: ['0xcafebabecafebabecafebabecafebabecafebabe'], +// symbol: 'AlphaUSD', +// value: '0x5f5e100', +// }], +// } + +result.capabilities.autoSwap +// { +// maxIn: { +// decimals: 6, +// formatted: '105.000000', +// name: 'AlphaUSD', +// symbol: 'AlphaUSD', +// token: '0x20c0000000000000000000000000000000000001', +// value: '0x6422c40', +// }, +// minOut: { +// decimals: 6, +// formatted: '100.000000', +// name: 'USDC.e', +// symbol: 'USDC.e', +// token: '0x20c000000000000000000000b9537d11c60e8b50', +// value: '0x5f5e100', +// }, +// slippage: 0.05, +// } + +result.capabilities.fee +// { +// amount: '0x6b86', +// decimals: 6, +// formatted: '0.027526', +// symbol: 'pathUSD', +// } ``` diff --git a/src/pages/accounts/server/handler.feePayer.mdx b/src/pages/accounts/server/handler.feePayer.mdx index 0ff5cf43..4baf405b 100644 --- a/src/pages/accounts/server/handler.feePayer.mdx +++ b/src/pages/accounts/server/handler.feePayer.mdx @@ -127,3 +127,23 @@ const handler = Handler.feePayer({ }, // [!code focus] }) ``` + + +### validate + +- **Type:** `(request: TransactionRequest) => boolean | Promise` +- **Optional** + +Validates whether to sponsor a transaction. When omitted, all transactions are sponsored. Return `false` to reject sponsorship — the handler will re-fill without `feePayer` so gas/nonce are correct for self-payment. + +```ts twoslash +import { privateKeyToAccount } from 'viem/accounts' +import { Handler } from 'accounts/server' + +const blocked = '0x...' + +const handler = Handler.feePayer({ + account: privateKeyToAccount('0x...'), + validate: (request) => request.from !== blocked, // [!code focus] +}) +``` \ No newline at end of file diff --git a/src/pages/accounts/server/handler.relay.mdx b/src/pages/accounts/server/handler.relay.mdx new file mode 100644 index 00000000..4a21a286 --- /dev/null +++ b/src/pages/accounts/server/handler.relay.mdx @@ -0,0 +1,549 @@ +--- +title: Handler.relay +description: Server handler that proxies certain RPC requests with wallet-aware enrichment. +--- + +import { Cards, Card } from 'vocs' + +# `Handler.relay` + +Creates a server handler that proxies certain RPC requests (like `eth_fillTransaction`) with wallet-aware enrichment — fee token resolution, simulation-based balance diffs, conditional sponsorship, and automatic AMM resolution for insufficient balances. + +## Usage + +```ts twoslash +import { Handler } from 'accounts/server' + +const handler = Handler.relay() +``` + +Then plug `handler` into your server framework of choice: + +```ts twoslash +// @noErrors +import { Handler } from 'accounts/server' +const handler = Handler.relay() +// ---cut--- +createServer(handler.listener) // Node.js +Bun.serve(handler) // Bun +Deno.serve(handler) // Deno +app.all('*', c => handler.fetch(c.request)) // Elysia +app.use(handler.listener) // Express +app.use(c => handler.fetch(c.req.raw)) // Hono +export const GET = handler.fetch // Next.js +export const POST = handler.fetch // Next.js +``` + +## Features + + + + + + + + + +### Sponsorship + +Configure a [`feePayer`](#feepayer) to sponsor transactions. The relay signs `feePayerSignature` on the filled transaction and returns sponsor details in the response metadata. Use a [`validate`](#feepayervalidate) callback for conditional sponsorship — rejected transactions are re-filled for self-payment. + +:::code-group + +```ts twoslash [server.ts] +import { privateKeyToAccount } from 'viem/accounts' +import { Handler } from 'accounts/server' + +const blockedAddress = '0x...' +// ---cut--- +const handler = Handler.relay({ + feePayer: { + account: privateKeyToAccount('0x...'), + name: 'My App', + url: 'https://myapp.com', + // Optional — validate sponsorship approval. + validate: (request) => request.from !== blockedAddress, + }, +}) +``` + +```ts twoslash [client.ts] +import { Provider } from 'accounts' +const provider = Provider.create() +const [account] = await provider.request({ method: 'eth_accounts' }) +// ---cut--- +const result = await provider.request({ + method: 'eth_fillTransaction', + params: [{ + from: account, + calls: [{ + to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e + // transfer 100 USDC.e + data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', + }], + }], +}) + +result.capabilities.sponsored +// true + +result.capabilities.sponsor +// { +// address: '0x1234567890abcdef1234567890abcdef12345678', +// name: 'My App', +// url: 'https://myapp.com', +// } +``` + +::: + +### Auto Swap + +When a user has insufficient balance of a required token, the relay automatically injects swap calls (approve + buy) via the [Stablecoin DEX](/guide/stablecoin-dex). Swap details are reported in `capabilities.autoSwap`, and swap-related balance diffs are excluded from `capabilities.balanceDiffs`. Configurable via [`autoSwap`](#autoswap). + +:::code-group + +```ts twoslash [client.ts] +import { Provider } from 'accounts' +const provider = Provider.create() +const [account] = await provider.request({ method: 'eth_accounts' }) +// ---cut--- +const result = await provider.request({ + method: 'eth_fillTransaction', + params: [{ + from: account, + calls: [{ + to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e + // transfer 100 USDC.e + data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', + }], + }], +}) + +result.capabilities.autoSwap +// { +// maxIn: { +// decimals: 6, +// formatted: '105.000000', +// name: 'AlphaUSD', +// symbol: 'AlphaUSD', +// token: '0x20c0000000000000000000000000000000000001', +// value: '0x6422c40', +// }, +// minOut: { +// decimals: 6, +// formatted: '100.000000', +// name: 'USDC.e', +// symbol: 'USDC.e', +// token: '0x20c000000000000000000000b9537d11c60e8b50', +// value: '0x5f5e100', +// }, +// slippage: 0.05, +// } +``` + +```ts [server.ts] +import { Handler } from 'accounts/server' + +const handler = Handler.relay({ + autoSwap: { slippage: 0.05 }, // 5% (default) +}) +``` + +::: + +### Best Fee Tokens + +Picks the user's optimal `feeToken` automatically: + +1. On-chain preference via `fee.getUserToken` (if set and has balance) +2. Highest-balance token from the [`resolveTokens`](#resolvetokens) list +3. Validator fallback + +```ts twoslash +import { Provider } from 'accounts' +const provider = Provider.create() +const [account] = await provider.request({ method: 'eth_accounts' }) +// ---cut--- +const result = await provider.request({ + method: 'eth_fillTransaction', + params: [{ + from: account, + calls: [{ + to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e + // transfer 100 USDC.e + data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', + }], + }], +}) + +result.capabilities.fee +// { +// amount: '0x6b86', +// decimals: 6, +// formatted: '0.027526', +// symbol: 'USDC.e', +// } +``` + +### Balance Diffs + +Simulates the transaction and returns per-account token balance diffs in `capabilities.balanceDiffs`. + +:::code-group + +```ts twoslash [client.ts] +import { Provider } from 'accounts' +const provider = Provider.create() +const [account] = await provider.request({ method: 'eth_accounts' }) +// ---cut--- +const result = await provider.request({ + method: 'eth_fillTransaction', + params: [{ + from: account, + calls: [{ + to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e + // transfer 100 USDC.e + data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', + }], + }], +}) + +result.capabilities.balanceDiffs +// { +// '0x1234567890abcdef1234567890abcdef12345678': [{ +// address: '0x20c000000000000000000000b9537d11c60e8b50', +// decimals: 6, +// direction: 'outgoing', +// formatted: '100.000000', +// name: 'USDC.e', +// recipients: ['0xcafebabecafebabecafebabecafebabecafebabe'], +// symbol: 'USDC.e', +// value: '0x5f5e100', +// }], +// } +``` + +```ts twoslash [server.ts] +import { Handler } from 'accounts/server' +// ---cut--- +// Balance diffs are included by default — no extra config needed. +const handler = Handler.relay() +``` + +::: + +### Fee Derivation + +Derives fee estimates from the filled transaction and returns them as both raw and human-readable values, so the UI can display costs without additional computation. + +:::code-group + +```ts twoslash [client.ts] +import { Provider } from 'accounts' +const provider = Provider.create() +const [account] = await provider.request({ method: 'eth_accounts' }) +// ---cut--- +const result = await provider.request({ + method: 'eth_fillTransaction', + params: [{ + from: account, + calls: [{ + to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e + // transfer 100 USDC.e + data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', + }], + }], +}) + +result.capabilities.fee +// { +// amount: '0x6b86', +// decimals: 6, +// formatted: '0.027526', +// symbol: 'pathUSD', +// } +``` + +```ts twoslash [server.ts] +import { Handler } from 'accounts/server' +// ---cut--- +// Formatted fees are included by default — no extra config needed. +const handler = Handler.relay() +``` + +::: + +## Parameters + +### autoSwap + +- **Type:** `false | { slippage?: number }` +- **Default:** `{}` + +AMM swap options for automatic insufficient balance resolution. When a user doesn't hold enough of a token, the relay auto-swaps from their fee token via the [Stablecoin DEX](/guide/stablecoin-dex). Set to `false` to disable. + +```ts twoslash +import { Handler } from 'accounts/server' + +const handler = Handler.relay({ + autoSwap: { slippage: 0.02 }, // 2% slippage // [!code focus] +}) +``` + +#### autoSwap.slippage + +- **Type:** `number` +- **Default:** `0.05` (5%) + +Slippage tolerance for AMM swaps. The relay sets `maxAmountIn = deficit + deficit * slippage`. + +### chains + +- **Type:** `readonly [Chain, ...Chain[]]` +- **Default:** `[tempo, tempoModerato]` + +Supported chains. The handler resolves the client based on the `chainId` in the incoming transaction. + +```ts twoslash +import { tempo } from 'viem/chains' +import { Handler } from 'accounts/server' + +const handler = Handler.relay({ + chains: [tempo], // [!code focus] +}) +``` + +### feePayer + +- **Type:** `object` +- **Optional** + +Fee payer configuration. When provided, the relay will sign `feePayerSignature` on the filled transaction. + +```ts twoslash +import { privateKeyToAccount } from 'viem/accounts' +import { Handler } from 'accounts/server' + +const handler = Handler.relay({ + feePayer: { // [!code focus] + account: privateKeyToAccount('0x...'), // [!code focus] + name: 'My App', // [!code focus] + url: 'https://myapp.com', // [!code focus] + }, // [!code focus] +}) +``` + +#### feePayer.account + +- **Type:** `LocalAccount` +- **Required** + +The account to use as the fee payer. + +#### feePayer.name + +- **Type:** `string` +- **Optional** + +Sponsor display name returned in the response metadata. + +#### feePayer.url + +- **Type:** `string` +- **Optional** + +Sponsor URL returned in the response metadata. + +#### feePayer.validate + +- **Type:** `(request: TransactionRequest) => boolean | Promise` +- **Optional** + +Validates whether to sponsor a transaction. When omitted, all transactions are sponsored. Return `false` to reject sponsorship — the relay will re-fill without `feePayer` so gas/nonce are correct for self-payment. + +```ts twoslash +import { privateKeyToAccount } from 'viem/accounts' +import { Handler } from 'accounts/server' + +const blocked = '0x...' + +const handler = Handler.relay({ + feePayer: { + account: privateKeyToAccount('0x...'), + validate: (request) => request.from !== blocked, // [!code focus] + }, +}) +``` + +### onRequest + +- **Type:** `(request: RpcRequest) => Promise` +- **Optional** + +Callback called before processing each request. Useful for logging, rate limiting, or custom validation. + +```ts twoslash +import { Handler } from 'accounts/server' + +const handler = Handler.relay({ + onRequest: async (request) => { // [!code focus] + console.log('Processing request:', request.method) // [!code focus] + }, // [!code focus] +}) +``` + +### path + +- **Type:** `string` +- **Default:** `'/'` + +Path where the handler listens for requests. + +```ts twoslash +import { Handler } from 'accounts/server' + +const handler = Handler.relay({ + path: '/relay', // [!code focus] +}) +``` + +### resolveTokens + +- **Type:** `(chainId?: number) => readonly Address[]` +- **Optional** + +Returns token addresses to check balances for during fee token resolution. The relay checks `balanceOf` for each token and picks the one with the highest balance. + +```ts twoslash +import { Handler } from 'accounts/server' + +const handler = Handler.relay({ + resolveTokens: (chainId) => [ // [!code focus] + '0x20c0000000000000000000000000000000000000', // pathUSD // [!code focus] + '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e // [!code focus] + ], // [!code focus] +}) +``` + +### transports + +- **Type:** `Record` +- **Default:** `http()` for each chain + +Transports keyed by chain ID. + +```ts twoslash +import { http } from 'viem' +import { tempo } from 'viem/chains' +import { Handler } from 'accounts/server' + +const handler = Handler.relay({ + transports: { // [!code focus] + [tempo.id]: http('https://rpc.tempo.xyz'), // [!code focus] + }, // [!code focus] +}) +``` + +The relay enriches the standard `eth_fillTransaction` response with a `capabilities` object: + +```ts +type Response = { + /** Wallet-specific capabilities computed during fill. */ + capabilities: { + /** Per-account balance diffs from simulation (swap-related diffs excluded). */ + balanceDiffs?: { + [account: Address]: BalanceDiff[] + } + /** Fee estimate for the transaction. */ + fee: { + /** Raw fee amount in token units (hex-encoded). */ + amount: Hex + /** Token decimals (e.g. 6). */ + decimals: number + /** Human-readable fee (e.g. "0.028022"). */ + formatted: string + /** Token symbol (e.g. "AlphaUSD"). */ + symbol: string + } | null + /** AMM swap injected to cover an insufficient balance. */ + autoSwap?: { + /** Max input amount with slippage. */ + maxIn: SwapAmount + /** Deficit amount that triggered the swap. */ + minOut: SwapAmount + /** Slippage tolerance (e.g. 0.05 = 5%). */ + slippage: number + } + /** Sponsor details (when sponsored). */ + sponsor?: { address: Address; name: string; url: string } + /** Whether the transaction is sponsored by a fee payer. */ + sponsored: boolean + } + /** Fully filled transaction. */ + tx: { + // ...filled tx fields + /** Resolved fee token used for this transaction. */ + feeToken: Address + } +} + +type BalanceDiff = { + /** Token address. */ + address: Address + /** Token decimals (e.g. 6). */ + decimals: number + /** Direction relative to the user. */ + direction: 'incoming' | 'outgoing' + /** Human-readable formatted amount (e.g. "100.00"). */ + formatted: string + /** Token name (e.g. "USDC.e"). */ + name: string + /** Addresses receiving this asset. */ + recipients: readonly Address[] + /** Token symbol (e.g. "USDC.e"). */ + symbol: string + /** Token amount (hex-encoded). */ + value: Hex +} + +type SwapAmount = { + /** Token decimals. */ + decimals: number + /** Human-readable formatted amount. */ + formatted: string + /** Token name (e.g. "AlphaUSD"). */ + name: string + /** Token symbol (e.g. "AlphaUSD"). */ + symbol: string + /** Token address. */ + token: Address + /** Amount (hex-encoded). */ + value: Hex +} +``` diff --git a/vocs.config.ts b/vocs.config.ts index 9abd6967..81a4baf3 100644 --- a/vocs.config.ts +++ b/vocs.config.ts @@ -928,6 +928,10 @@ export default defineConfig({ text: '.feePayer', link: '/accounts/server/handler.feePayer', }, + { + text: '.relay', + link: '/accounts/server/handler.relay', + }, { text: '.webAuthn', link: '/accounts/server/handler.webAuthn', @@ -994,8 +998,7 @@ export default defineConfig({ link: '/accounts/rpc/eth_sendTransactionSync', }, { - text: 'eth_fillTransaction 🚧', - disabled: true, + text: 'eth_fillTransaction', link: '/accounts/rpc/eth_fillTransaction', }, { From 2a872ee707c8953197c6c732a4aa15100917b9ff Mon Sep 17 00:00:00 2001 From: jxom Date: Mon, 13 Apr 2026 16:58:43 +1000 Subject: [PATCH 2/3] chore: bump accounts to 0.6.2 Amp-Thread-ID: https://ampcode.com/threads/T-019d856e-ab9f-74a3-bc14-5e5971998170 --- package.json | 2 +- pnpm-lock.yaml | 94 ++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 76 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 7ec58121..222b4f9b 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "@vercel/analytics": "^1.6.1", "@vercel/speed-insights": "^1.3.1", "abitype": "^1.2.3", - "accounts": "^0.6.0", + "accounts": "^0.6.2", "cva": "1.0.0-beta.4", "mermaid": "^11.12.2", "monaco-editor": "^0.55.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3522eedc..bad4fa97 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,8 +38,8 @@ importers: specifier: ^1.2.3 version: 1.2.3(typescript@5.9.3)(zod@4.3.5) accounts: - specifier: ^0.6.0 - version: 0.6.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.5))(@types/react@19.2.9)(@wagmi/core@3.4.0(@tanstack/query-core@5.90.19)(@types/react@19.2.9)(ox@0.14.10(typescript@5.9.3)(zod@4.3.5))(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.47.10(typescript@5.9.3)(zod@4.3.5)))(express@5.2.1)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.3))(viem@2.47.10(typescript@5.9.3)(zod@4.3.5)) + specifier: ^0.6.2 + version: 0.6.2(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.5))(@types/react@19.2.9)(@wagmi/core@3.4.0(@tanstack/query-core@5.90.19)(@types/react@19.2.9)(ox@0.14.10(typescript@5.9.3)(zod@4.3.5))(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.47.10(typescript@5.9.3)(zod@4.3.5)))(express@5.2.1)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.3))(viem@2.47.10(typescript@5.9.3)(zod@4.3.5)) cva: specifier: 1.0.0-beta.4 version: 1.0.0-beta.4(typescript@5.9.3) @@ -1399,8 +1399,8 @@ packages: '@types/node@25.0.10': resolution: {integrity: sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==} - '@types/node@25.5.0': - resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} + '@types/node@25.6.0': + resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} @@ -1644,8 +1644,8 @@ packages: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} - accounts@0.6.0: - resolution: {integrity: sha512-2UsnjMUTYJiGI+c60rKf5bz0uzyWlvwBK+0jn6kU/c4PD4U7SshIbvLRnI5yW6lI5GUtffkQD6o2v9hT3Sshzw==} + accounts@0.6.2: + resolution: {integrity: sha512-Xz9F1GQ19C7rR52C9mjdYdE0cW2vGR22F97BW3M6hzaeyWzyAuSkvRTOb3hnkc4U5oxZaKtvAgwrP1wQDtW7bw==} peerDependencies: '@react-native-async-storage/async-storage': ^3.0.2 '@wagmi/core': '>=2' @@ -1736,6 +1736,11 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.10.18: + resolution: {integrity: sha512-VSnGQAOLtP5mib/DPyg2/t+Tlv65NTBz83BJBJvmLVHHuKJVaDOBvJJykiT5TR++em5nfAySPccDZDa4oSrn8A==} + engines: {node: '>=6.0.0'} + hasBin: true + baseline-browser-mapping@2.9.17: resolution: {integrity: sha512-agD0MgJFUP/4nvjqzIB29zRPUuCF7Ge6mEv9s8dHrtYD7QWXRcx75rOADE/d5ah1NI+0vkDl0yorDd5U852IQQ==} hasBin: true @@ -1749,6 +1754,11 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -1782,6 +1792,9 @@ packages: caniuse-lite@1.0.30001765: resolution: {integrity: sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==} + caniuse-lite@1.0.30001787: + resolution: {integrity: sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -2143,6 +2156,9 @@ packages: electron-to-chromium@1.5.277: resolution: {integrity: sha512-wKXFZw4erWmmOz5N/grBoJ2XrNJGDFMu2+W5ACHza5rHtvsqrK4gb6rnLC7XxKB9WlJ+RmyQatuEXmtm86xbnw==} + electron-to-chromium@1.5.335: + resolution: {integrity: sha512-q9n5T4BR4Xwa2cwbrwcsDJtHD/enpQ5S1xF1IAtdqf5AAgqDFmR/aakqH3ChFdqd/QXJhS3rnnXFtexU7rax6Q==} + encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -3041,6 +3057,9 @@ packages: node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + node-releases@2.0.37: + resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} + node@runtime:24.12.0: resolution: type: variations @@ -3191,8 +3210,8 @@ packages: typescript: optional: true - ox@0.14.13: - resolution: {integrity: sha512-N3slDyEUq3qGw/53Xd8YZPZD7NUbbiOJDeWKvQ1ElNo2mFjjz6cV2TIbGenHw7k5ATcefDQh42dwUWoGtxU9Hg==} + ox@0.14.15: + resolution: {integrity: sha512-3TubCmbKen/cuZQzX0qDbOS5lojjdSZ90lqKxWIDWd5siuJ0IJBaTXMYs8eMPLcraqnOwGZazz3apHPGiRCkGQ==} peerDependencies: typescript: '>=5.4.0' peerDependenciesMeta: @@ -3755,8 +3774,8 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - undici-types@7.18.2: - resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + undici-types@7.19.2: + resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} unicorn-magic@0.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} @@ -5421,9 +5440,9 @@ snapshots: dependencies: undici-types: 7.16.0 - '@types/node@25.5.0': + '@types/node@25.6.0': dependencies: - undici-types: 7.18.2 + undici-types: 7.19.2 '@types/react-dom@19.2.3(@types/react@19.2.9)': dependencies: @@ -5635,13 +5654,13 @@ snapshots: mime-types: 3.0.2 negotiator: 1.0.0 - accounts@0.6.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.5))(@types/react@19.2.9)(@wagmi/core@3.4.0(@tanstack/query-core@5.90.19)(@types/react@19.2.9)(ox@0.14.10(typescript@5.9.3)(zod@4.3.5))(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.47.10(typescript@5.9.3)(zod@4.3.5)))(express@5.2.1)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.3))(viem@2.47.10(typescript@5.9.3)(zod@4.3.5)): + accounts@0.6.2(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.5))(@types/react@19.2.9)(@wagmi/core@3.4.0(@tanstack/query-core@5.90.19)(@types/react@19.2.9)(ox@0.14.10(typescript@5.9.3)(zod@4.3.5))(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.47.10(typescript@5.9.3)(zod@4.3.5)))(express@5.2.1)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.3))(viem@2.47.10(typescript@5.9.3)(zod@4.3.5)): dependencies: hono: 4.12.12 idb-keyval: 6.2.2 mipd: 0.0.7(typescript@5.9.3) mppx: 0.5.10(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.5))(express@5.2.1)(hono@4.12.12)(typescript@5.9.3)(viem@2.47.10(typescript@5.9.3)(zod@4.3.5)) - ox: 0.14.13(typescript@5.9.3)(zod@4.3.6) + ox: 0.14.15(typescript@5.9.3)(zod@4.3.6) webauthx: 0.1.0(typescript@5.9.3)(zod@4.3.6) zod: 4.3.6 zustand: 5.0.12(@types/react@19.2.9)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)) @@ -5714,6 +5733,8 @@ snapshots: base64-js@1.5.1: {} + baseline-browser-mapping@2.10.18: {} + baseline-browser-mapping@2.9.17: {} body-parser@2.2.2: @@ -5738,6 +5759,14 @@ snapshots: node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.18 + caniuse-lite: 1.0.30001787 + electron-to-chromium: 1.5.335 + node-releases: 2.0.37 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + buffer-from@1.1.2: {} buffer@6.0.3: @@ -5765,6 +5794,8 @@ snapshots: caniuse-lite@1.0.30001765: {} + caniuse-lite@1.0.30001787: {} + ccount@2.0.1: {} change-case@5.4.4: {} @@ -6117,6 +6148,8 @@ snapshots: electron-to-chromium@1.5.277: {} + electron-to-chromium@1.5.335: {} + encodeurl@2.0.0: {} enhanced-resolve@5.20.1: @@ -6637,7 +6670,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 25.5.0 + '@types/node': 25.6.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -7353,6 +7386,8 @@ snapshots: node-releases@2.0.27: {} + node-releases@2.0.37: {} + node@runtime:24.12.0: {} npm-run-path@6.0.0: @@ -7404,7 +7439,22 @@ snapshots: transitivePeerDependencies: - zod - ox@0.14.13(typescript@5.9.3)(zod@4.3.6): + ox@0.14.10(typescript@5.9.3)(zod@4.3.6): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.2.3(typescript@5.9.3)(zod@4.3.6) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - zod + + ox@0.14.15(typescript@5.9.3)(zod@4.3.6): dependencies: '@adraffy/ens-normalize': 1.11.1 '@noble/ciphers': 1.3.0 @@ -8123,7 +8173,7 @@ snapshots: undici-types@7.16.0: {} - undici-types@7.18.2: {} + undici-types@7.19.2: {} unicorn-magic@0.3.0: {} @@ -8242,6 +8292,12 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + urlpattern-polyfill@10.1.0: {} use-sync-external-store@1.4.0(react@19.2.3): @@ -8493,7 +8549,7 @@ snapshots: webauthx@0.1.0(typescript@5.9.3)(zod@4.3.6): dependencies: - ox: 0.14.13(typescript@5.9.3)(zod@4.3.6) + ox: 0.14.10(typescript@5.9.3)(zod@4.3.6) transitivePeerDependencies: - typescript - zod @@ -8512,7 +8568,7 @@ snapshots: '@webassemblyjs/wasm-parser': 1.14.1 acorn: 8.16.0 acorn-import-phases: 1.0.4(acorn@8.16.0) - browserslist: 4.28.1 + browserslist: 4.28.2 chrome-trace-event: 1.0.4 enhanced-resolve: 5.20.1 es-module-lexer: 2.0.0 From 2eb3af051079e62fa342f1da1b085759835a9d35 Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Wed, 15 Apr 2026 15:51:32 +1000 Subject: [PATCH 3/3] docs: add features option, error details, and requireFunds to relay docs Amp-Thread-ID: https://ampcode.com/threads/T-019d8f1f-593e-72a9-83ab-36136df98e2e --- .../accounts/rpc/eth_fillTransaction.mdx | 94 +++++++++++++++ src/pages/accounts/server/handler.relay.mdx | 113 +++++++++++++++++- 2 files changed, 204 insertions(+), 3 deletions(-) diff --git a/src/pages/accounts/rpc/eth_fillTransaction.mdx b/src/pages/accounts/rpc/eth_fillTransaction.mdx index a3ebff09..8cab5f28 100644 --- a/src/pages/accounts/rpc/eth_fillTransaction.mdx +++ b/src/pages/accounts/rpc/eth_fillTransaction.mdx @@ -85,6 +85,28 @@ type Response = { symbol: string } + /** Structured error details when the fill fails. */ + error?: { + /** Revert error name (e.g. "InsufficientBalance"). */ + errorName: string + /** Human-readable error message. */ + message: string + } + + /** Funding requirement when InsufficientBalance is encountered. */ + requireFunds?: { + /** Deficit amount in token units. */ + amount: `0x${string}` + /** Token decimals (e.g. 6). */ + decimals: number + /** Human-readable deficit (e.g. "100.000000"). */ + formatted: string + /** Token address. */ + token: `0x${string}` + /** Token symbol (e.g. "USDC.e"). */ + symbol: string + } + /** Sponsor details, present when `sponsored` is `true`. */ sponsor?: { /** Sponsor address. */ @@ -312,3 +334,75 @@ result.capabilities.fee // symbol: 'pathUSD', // } ``` + +### Error Details + +When `eth_fillTransaction` fails, the relay returns structured error details in `capabilities.error` instead of throwing. + +```ts twoslash +import { Provider } from 'accounts' +const provider = Provider.create() +const [account] = await provider.request({ method: 'eth_accounts' }) +// ---cut--- +const result = await provider.request({ + method: 'eth_fillTransaction', + params: [{ + from: account, + calls: [{ + to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e + data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', + }], + }], +}) + +result.capabilities.error +// { +// errorName: 'BelowMinimumOrderSize', +// message: 'Below minimum order size: 1000000.', +// } +``` + +### Require Funds + +For `InsufficientBalance` errors, the relay also returns a `capabilities.requireFunds` object with the exact deficit amount and token metadata. + +```ts twoslash +import { Provider } from 'accounts' +const provider = Provider.create() +const [account] = await provider.request({ method: 'eth_accounts' }) +// ---cut--- +const result = await provider.request({ + method: 'eth_fillTransaction', + params: [{ + from: account, + calls: [{ + to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e + // transfer 100 USDC.e (but account has 0) + data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', + }], + }], +}) + +result.capabilities.requireFunds +// { +// amount: '0x5f5e100', +// decimals: 6, +// formatted: '100.000000', +// token: '0x20c000000000000000000000b9537d11c60e8b50', +// symbol: 'USDC.e', +// } + +result.capabilities.balanceDiffs +// { +// '0x1234567890abcdef1234567890abcdef12345678': [{ +// address: '0x20c000000000000000000000b9537d11c60e8b50', +// decimals: 6, +// direction: 'outgoing', +// formatted: '100.000000', +// name: 'USDC.e', +// recipients: ['0xcafebabecafebabecafebabecafebabecafebabe'], +// symbol: 'USDC.e', +// value: '0x5f5e100', +// }], +// } +``` diff --git a/src/pages/accounts/server/handler.relay.mdx b/src/pages/accounts/server/handler.relay.mdx index 4a21a286..5123d623 100644 --- a/src/pages/accounts/server/handler.relay.mdx +++ b/src/pages/accounts/server/handler.relay.mdx @@ -67,6 +67,12 @@ export const POST = handler.fetch // Next.js title="Fee Derivation" to="#fee-derivation" /> + ### Sponsorship @@ -124,7 +130,7 @@ result.capabilities.sponsor ### Auto Swap -When a user has insufficient balance of a required token, the relay automatically injects swap calls (approve + buy) via the [Stablecoin DEX](/guide/stablecoin-dex). Swap details are reported in `capabilities.autoSwap`, and swap-related balance diffs are excluded from `capabilities.balanceDiffs`. Configurable via [`autoSwap`](#autoswap). +When a user has insufficient balance of a required token, the relay automatically injects swap calls (approve + buy) via the [Stablecoin DEX](/guide/stablecoin-dex). Swap details are reported in `capabilities.autoSwap`, and swap-related balance diffs are excluded from `capabilities.balanceDiffs`. Enabled by [`features: 'all'`](#features), configurable via [`autoSwap`](#autoswap). :::code-group @@ -299,14 +305,73 @@ const handler = Handler.relay() ::: +### Require Funds + +For insufficient balance errors, the relay also returns a `capabilities.requireFunds` object with the exact deficit amount and token metadata — enabling UIs to prompt the user to fund their account. + +```ts twoslash +import { Provider } from 'accounts' +const provider = Provider.create() +const [account] = await provider.request({ method: 'eth_accounts' }) +// ---cut--- +const result = await provider.request({ + method: 'eth_fillTransaction', + params: [{ + from: account, + calls: [{ + to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e + // transfer 100 USDC.e (but account has 40) + data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', + }], + }], +}) + +result.capabilities.requireFunds +// { +// amount: '0x3938700', +// decimals: 6, +// formatted: '60.000000', +// token: '0x20c000000000000000000000b9537d11c60e8b50', +// symbol: 'USDC.e', +// } + +result.capabilities.balanceDiffs +// { +// '0x1234567890abcdef1234567890abcdef12345678': [{ +// address: '0x20c000000000000000000000b9537d11c60e8b50', +// decimals: 6, +// direction: 'outgoing', +// formatted: '100.000000', +// name: 'USDC.e', +// recipients: ['0xcafebabecafebabecafebabecafebabecafebabe'], +// symbol: 'USDC.e', +// value: '0x5f5e100', +// }], +// } +``` + +### Enabling Features + +By default, only a minimum set of features are enabled, given on what options you pass to `Handler.relay(){:js}` (e.g. `feePayer`, `autoSwap`, etc). + +Set [`features: 'all'`](#features) to enable all features by default such as: **fee token resolution**, **auto-swap**, and **simulation** (balance diffs + fee breakdown). This will come at the cost of slightly increased network latency. + +```ts twoslash +import { privateKeyToAccount } from 'viem/accounts' +import { Handler } from 'accounts/server' + +const handler = Handler.relay({ + features: 'all', // [!code focus] +}) +``` + ## Parameters ### autoSwap - **Type:** `false | { slippage?: number }` -- **Default:** `{}` -AMM swap options for automatic insufficient balance resolution. When a user doesn't hold enough of a token, the relay auto-swaps from their fee token via the [Stablecoin DEX](/guide/stablecoin-dex). Set to `false` to disable. +AMM swap options for automatic insufficient balance resolution. When a user doesn't hold enough of a token, the relay auto-swaps from their fee token via the [Stablecoin DEX](/guide/stablecoin-dex). Set to `false` to disable even when `features: 'all'` is set. ```ts twoslash import { Handler } from 'accounts/server' @@ -339,6 +404,24 @@ const handler = Handler.relay({ }) ``` +### features + +- **Type:** `'all'` +- **Optional** + +Controls which relay features are enabled. By default, only fee payer sponsorship is active. + +- `'all'`: enables all features (fee token resolution, auto-swap, balance diffs, fee breakdown, etc). +- `undefined` (default): enables only features that are configured via options (e.g. `feePayer`, `autoSwap`, etc). + +```ts twoslash +import { Handler } from 'accounts/server' + +const handler = Handler.relay({ + features: 'all', // [!code focus] +}) +``` + ### feePayer - **Type:** `object` @@ -500,6 +583,30 @@ type Response = { /** Slippage tolerance (e.g. 0.05 = 5%). */ slippage: number } + /** Structured error details when the fill fails (e.g. InsufficientBalance). */ + error?: { + /** ABI item that caused the error. */ + abiItem: AbiItem + /** Data that caused the error. */ + data: Hex + /** Revert error name (e.g. "InsufficientBalance"). */ + errorName: string + /** Human-readable error message. */ + message: string + } + /** Funding requirement when InsufficientBalance is encountered. */ + requireFunds?: { + /** Deficit amount in token units (hex-encoded). */ + amount: Hex + /** Token decimals (e.g. 6). */ + decimals: number + /** Human-readable deficit (e.g. "100.000000"). */ + formatted: string + /** Token address. */ + token: Address + /** Token symbol (e.g. "USDC.e"). */ + symbol: string + } /** Sponsor details (when sponsored). */ sponsor?: { address: Address; name: string; url: string } /** Whether the transaction is sponsored by a fee payer. */