/** @module-tag localnet */
import type { RpcRequest } from 'ox'
import { SignatureEnvelope, TxEnvelopeTempo } from 'ox/tempo'
import { parseUnits, type Address, type BaseError } from 'viem'
import { fillTransaction, sendTransactionSync } from 'viem/actions'
import { Actions, Addresses, Capabilities, Tick, Transaction, VirtualAddress } from 'viem/tempo'
import { tempo, tempoModerato } from 'viem/tempo/chains'
import * as Relay from '../../test/Relay.js'
import * as Runtime from '../../test/runtime.js'
type Server = Relay.Server
const userAccount = Relay.accounts[9]!
const feePayerAccount = Relay.accounts[0]!
const recipient = Relay.accounts[7]!
/**
* Tokens the relay handler probes for fee-token resolution. The default
* `resolveTokens` uses the Tempo API for mainnet and testnet, so localnet
* tests inject this list explicitly.
*/
const localnetTokens = [
{
address: '0x20c0000000000000000000000000000000000000',
decimals: 6,
name: 'pathUSD',
symbol: 'pathUSD',
},
{
address: '0x20c0000000000000000000000000000000000001',
decimals: 6,
name: 'alphaUSD',
symbol: 'alphaUSD',
},
{
address: '0x20c0000000000000000000000000000000000002',
decimals: 6,
name: 'betaUSD',
symbol: 'betaUSD',
},
{
address: '0x20c0000000000000000000000000000000000003',
decimals: 6,
name: 'thetaUSD',
symbol: 'thetaUSD',
},
] as const
/** Case-insensitive lookup into balanceDiffs keyed by address. */
function findDiffs(
balanceDiffs: Capabilities.FillTransactionCapabilities['balanceDiffs'],
address: string,
) {
return Object.entries(balanceDiffs ?? {}).find(
([addr]) => addr.toLowerCase() === address.toLowerCase(),
)?.[1]
}
/** Extracts relay virtual-address metadata while viem's public type catches up. */
function virtualAddresses(capabilities: Capabilities.FillTransactionCapabilities | undefined) {
return (
capabilities as
| (Capabilities.FillTransactionCapabilities & {
virtualAddresses?: Record
| undefined
})
| undefined
)?.virtualAddresses
}
/** Client used only to build typed calldata (`Actions.token.*.call`). */
const caller = Relay.getClient()
/** A simple transfer call for tests that just need a valid transaction. */
const transferCall = () =>
Actions.token.transfer.call(caller, {
token: Relay.addresses.alphaUsd,
to: recipient.address,
amount: 1n,
})
beforeAll(async () => {
// Faucet-fund the accounts that pay fees or create fresh tokens. The faucet is
// permissionless, so no genesis privileges are required. Left unfunded on
// purpose: accounts[5]/[10] (balance-sensitive fee-resolution and
// insufficient-balance tests) and accounts[7] (recipient only).
await Promise.all(
[0, 2, 3, 4, 6, 8, 9].map((index) =>
Actions.faucet.fundSync(Relay.getClient(), {
account: Relay.accounts[index]!,
timeout: 60_000,
}),
),
)
// userAccount prefers alphaUsd (a faucet-funded genesis token) as its fee token.
await Actions.fee.setUserToken(Relay.getClient(), {
account: userAccount,
token: Relay.addresses.alphaUsd,
})
})
describe.skipIf(Runtime.get().mode !== 'localnet')('default', () => {
let client: ReturnType
let server: Server
beforeAll(async () => {
server = await Relay.createServer((await Relay.relay({})).listener)
client = Relay.getClient({ url: server.url })
})
afterAll(() => {
server.close()
})
test('default: returns filled transaction with capabilities', async () => {
const { transaction } = await fillTransaction(client, {
account: userAccount.address,
calls: [transferCall()],
})
expect(transaction.gas).toBeDefined()
expect(transaction.nonce).toBeDefined()
})
test('behavior: proxies other methods to RPC node', async () => {
const chainId = await client.request({ method: 'eth_chainId' })
expect(Number(chainId)).toMatchInlineSnapshot(`${Relay.chain.id}`)
})
test('behavior: surfaces upstream RPC errors as JSON-RPC errors', async () => {
// grantRoles reverts with Unauthorized when caller is not an admin
// (eth_call defaults `from` to the zero address). We expect the relay to
// forward the revert as a structured JSON-RPC error response (HTTP 200
// with `error.code`/`error.data`), not a 500.
const call = Actions.token.grantRoles.call(caller, {
token: Relay.addresses.alphaUsd,
role: 'issuer',
to: recipient.address,
})
const response = await fetch(server.url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'eth_call',
params: [{ to: call.to, data: call.data }, 'latest'],
}),
})
expect(response.status).toBe(200)
const body = (await response.json()) as {
id: number
jsonrpc: string
error: { code: number; message: string; data?: string }
}
// Drop `message` from the snapshot — it embeds the upstream RPC URL/port
// and viem version, which are nondeterministic.
const { message: _message, ...errorRest } = body.error
expect({ ...body, error: errorRest }).toMatchInlineSnapshot(`
{
"error": {
"code": 3,
"data": "0x82b42900",
},
"id": 1,
"jsonrpc": "2.0",
}
`)
})
test('behavior: returns actionable error for eth_signRawTransaction without feePayer', async () => {
const response = await fetch(server.url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'eth_signRawTransaction',
params: ['0x00'],
}),
})
expect(response.status).toBe(200)
const body = (await response.json()) as {
id: number
jsonrpc: string
error: { code: number; message: string }
}
expect(body.error.code).toBe(-32601)
expect(body.error.message).toContain('fee payer')
expect(body.error.message).toContain('Handler.relay()')
})
test('behavior: handles JSON-RPC batch requests', async () => {
const response = await fetch(server.url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify([
{ jsonrpc: '2.0', id: 1, method: 'eth_chainId', params: [] },
{ jsonrpc: '2.0', id: 2, method: 'eth_chainId', params: [] },
]),
})
expect(response.status).toBe(200)
const body = (await response.json()) as { id: number; result: string }[]
expect(Array.isArray(body)).toBe(true)
expect(body).toHaveLength(2)
expect(body[0]!.id).toBe(1)
expect(body[1]!.id).toBe(2)
expect(Number(body[0]!.result)).toBe(Relay.chain.id)
expect(Number(body[1]!.result)).toBe(Relay.chain.id)
})
})
describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: with feePayer', () => {
let server: Server
let client: ReturnType
let requests: RpcRequest.RpcRequest[] = []
beforeAll(async () => {
server = await Relay.createServer(
(
await Relay.relay({
feePayer: {
account: feePayerAccount,
name: 'Test Sponsor',
url: 'https://test.com',
},
onRequest: async (request) => {
requests.push(request)
},
})
).listener,
)
client = Relay.getClient({ url: server.url })
})
afterAll(() => {
server.close()
})
afterEach(() => {
requests = []
})
test('default: returns sponsored tx with feePayerSignature', async () => {
const { transaction } = await fillTransaction(client, {
account: userAccount.address,
calls: [transferCall()],
})
expect(transaction.feePayerSignature).toBeDefined()
expect(requests.map(({ method }) => method)).toMatchInlineSnapshot(`
[
"eth_fillTransaction",
]
`)
})
test('behavior: returns sponsor capabilities', async () => {
const result = await fillTransaction(client, {
account: userAccount.address,
calls: [transferCall()],
})
const meta = result.capabilities
expect(meta?.['sponsored']).toBe(true)
expect(meta?.['sponsor']).toMatchInlineSnapshot(`
{
"address": "${feePayerAccount.address}",
"name": "Test Sponsor",
"url": "https://test.com",
}
`)
})
test('behavior: sponsored tx can be signed and broadcast', async () => {
const { transaction } = await fillTransaction(client, {
account: userAccount.address,
calls: [transferCall()],
})
const signed = await userAccount.signTransaction(transaction as never)
const receipt = (await Relay.getClient().request({
method: 'eth_sendRawTransactionSync' as never,
params: [signed],
})) as { feePayer?: string | undefined }
expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
})
test('behavior: missing from returns error capability when errors capability is enabled', async () => {
const result = await fillTransaction(client, {
calls: [transferCall()],
capabilities: { errors: true },
})
expect(result.capabilities).toMatchInlineSnapshot(`
{
"error": {
"errorName": "unknown",
"message": "unknown account",
},
"sponsored": false,
}
`)
})
})
describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: with feePayer.feeToken', () => {
let server: Server
let client: ReturnType
const sponsorFeeToken = '0x20c0000000000000000000000000000000000000' as const // pathUSD
beforeAll(async () => {
server = await Relay.createServer(
(
await Relay.relay({
feePayer: {
account: feePayerAccount,
feeToken: sponsorFeeToken,
},
tokens: localnetTokens,
})
).listener,
)
client = Relay.getClient({ url: server.url })
})
afterAll(() => {
server.close()
})
test('default: sponsor.feeToken is used when request omits feeToken', async () => {
const { transaction } = await fillTransaction(client, {
account: userAccount.address,
calls: [transferCall()],
})
expect(transaction.feeToken?.toLowerCase()).toBe(sponsorFeeToken)
expect(transaction.feePayerSignature).toBeDefined()
})
test('behavior: sponsor.feeToken overrides request feeToken on sponsored fills', async () => {
const { transaction } = await fillTransaction(client, {
account: userAccount.address,
calls: [transferCall()],
feeToken: Relay.addresses.alphaUsd as Address,
})
expect(transaction.feeToken?.toLowerCase()).toBe(sponsorFeeToken)
expect(transaction.feePayerSignature).toBeDefined()
})
test('behavior: request feeToken wins when sponsorship is opted out via feePayer:false', async () => {
const { transaction } = await fillTransaction(client, {
account: userAccount.address,
calls: [transferCall()],
feePayer: false as never,
feeToken: Relay.addresses.alphaUsd as Address,
})
expect(transaction.feeToken?.toLowerCase()).toBe(Relay.addresses.alphaUsd.toLowerCase())
expect(transaction.feePayerSignature).toBeUndefined()
})
test('behavior: broadcast tx receipt records the sponsor.feeToken', async () => {
// Fill via relay (where override kicks in), then sign + broadcast manually.
// viem's `sendTransactionSync` with a hydrated account does local prep and
// would skip the relay's `eth_fillTransaction`, bypassing the override.
const { transaction } = await fillTransaction(client, {
account: userAccount.address,
calls: [transferCall()],
feeToken: Relay.addresses.alphaUsd as Address,
})
expect(transaction.feeToken?.toLowerCase()).toBe(sponsorFeeToken)
const signed = await userAccount.signTransaction(transaction as never)
const receipt = (await Relay.getClient().request({
method: 'eth_sendRawTransactionSync' as never,
params: [signed],
})) as { feePayer?: string | undefined; feeToken?: string | undefined }
expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
expect(receipt.feeToken?.toLowerCase()).toBe(sponsorFeeToken)
})
test('behavior: raw sponsor signing restores sponsor.feeToken when envelope omits feeToken', async () => {
const rpc = Relay.getClient({ account: userAccount })
const { transaction } = await fillTransaction(rpc, {
account: userAccount.address,
calls: [transferCall()],
feePayer: true as never,
})
const partial = await userAccount.signTransaction({ ...transaction, feePayer: true } as never)
expect([null, undefined]).toContain(
Transaction.deserialize(partial as `0x76${string}`).feeToken,
)
const response = await fetch(server.url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'eth_signRawTransaction',
params: [partial],
}),
})
const body = (await response.json()) as { result: `0x76${string}` }
const feeToken = Transaction.deserialize(body.result).feeToken as Address | undefined
expect(feeToken?.toLowerCase()).toBe(sponsorFeeToken)
})
test('behavior: raw sponsor signing restores token-list default before validation', async () => {
let feeToken_validated: Address | undefined
const customServer = await Relay.createServer(
(
await Relay.relay({
feePayer: {
account: feePayerAccount,
validate: (request) => {
feeToken_validated = request.feeToken as Address | undefined
return true
},
},
tokens: localnetTokens,
})
).listener,
)
const rpc = Relay.getClient({ account: userAccount })
const { transaction } = await fillTransaction(rpc, {
account: userAccount.address,
calls: [transferCall()],
feePayer: true as never,
})
const partial = await userAccount.signTransaction({ ...transaction, feePayer: true } as never)
try {
const response = await fetch(customServer.url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'eth_signRawTransaction',
params: [partial],
}),
})
const body = (await response.json()) as { result: `0x76${string}` }
const feeToken = Transaction.deserialize(body.result).feeToken as Address | undefined
expect(feeToken_validated?.toLowerCase()).toBe(sponsorFeeToken)
expect(feeToken?.toLowerCase()).toBe(sponsorFeeToken)
} finally {
customServer.close()
}
})
})
describe.skipIf(Runtime.get().mode !== 'localnet')(
'behavior: with app-provided feePayer URL',
() => {
let appServer: Server
let walletServer: Server
let client: ReturnType
beforeAll(async () => {
// App relay: has a fee payer account and signs transactions.
appServer = await Relay.createServer(
(
await Relay.relay({
feePayer: {
account: feePayerAccount,
name: 'App Sponsor',
url: 'https://app.example.com',
},
})
).listener,
)
// Wallet relay: no fee payer configured — proxies to app relay.
walletServer = await Relay.createServer(
(
await Relay.relay({
internal_allowUnsafeUrls: true,
})
).listener,
)
client = Relay.getClient({ url: walletServer.url })
})
afterAll(() => {
appServer.close()
walletServer.close()
})
test('default: proxies fill to app relay and returns sponsored tx', async () => {
const { transaction } = await fillTransaction(client, {
account: userAccount.address,
calls: [transferCall()],
feePayer: appServer.url as never,
})
expect(transaction.feePayerSignature).toBeDefined()
expect(transaction.gas).toBeDefined()
})
test('behavior: relays sponsor metadata from app relay', async () => {
const result = await fillTransaction(client, {
account: userAccount.address,
calls: [transferCall()],
feePayer: appServer.url as never,
})
expect(result.capabilities?.['sponsored']).toBe(true)
expect(result.capabilities?.['sponsor']).toMatchInlineSnapshot(`
{
"address": "${feePayerAccount.address}",
"name": "App Sponsor",
"url": "https://app.example.com",
}
`)
})
test('behavior: sponsored tx from app relay can be signed and broadcast', async () => {
const { transaction } = await fillTransaction(client, {
account: userAccount.address,
calls: [transferCall()],
feePayer: appServer.url as never,
})
const signed = await userAccount.signTransaction(transaction as never)
const receipt = (await Relay.getClient().request({
method: 'eth_sendRawTransactionSync' as never,
params: [signed],
})) as { feePayer?: string | undefined }
expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
})
},
)
describe.skipIf(Runtime.get().mode !== 'localnet')(
'behavior: with app-provided feePayer URL + autoSwap',
() => {
let appServer: Server
let walletServer: Server
let client: ReturnType
beforeAll(async () => {
// App relay sponsors fees AND has `features: 'all'` so it can recover
// from InsufficientBalance via autoSwap.
appServer = await Relay.createServer(
(
await Relay.relay({
features: 'all',
feePayer: {
account: feePayerAccount,
name: 'App Sponsor',
url: 'https://app.example.com',
},
})
).listener,
)
// Wallet relay forwards to the app relay; also has features:'all' so its
// own fill() can detect upstream `capabilities.error` as InsufficientBalance.
walletServer = await Relay.createServer(
(
await Relay.relay({
features: 'all',
internal_allowUnsafeUrls: true,
})
).listener,
)
client = Relay.getClient({ url: walletServer.url })
})
afterAll(() => {
appServer.close()
walletServer.close()
})
test('behavior: autoSwap recovers when external feePayer surfaces InsufficientBalance', async () => {
const sender = Relay.accounts[6]!
// Token pair + DEX liquidity. Use alphaUsd as the quote token so the
// relay can swap alphaUsd → base to cover the deficit.
const rpc = Relay.getClient({ account: Relay.accounts[0]! })
const { token: base } = await Actions.token.createSync(rpc, {
name: 'External Swap Base',
symbol: 'EXTBASE',
currency: 'USD',
quoteToken: Relay.addresses.alphaUsd,
})
await sendTransactionSync(rpc, {
calls: [
Actions.token.grantRoles.call(caller, {
token: base,
role: 'issuer',
to: rpc.account!.address,
}),
Actions.token.mint.call(caller, {
token: base,
to: rpc.account!.address,
amount: parseUnits('10000', 6),
}),
Actions.token.approve.call(caller, {
token: base,
spender: Addresses.stablecoinDex,
amount: parseUnits('10000', 6),
}),
Actions.token.approve.call(caller, {
token: Relay.addresses.alphaUsd,
spender: Addresses.stablecoinDex,
amount: parseUnits('10000', 6),
}),
],
})
await Actions.dex.createPairSync(rpc, { base })
await Actions.dex.placeSync(rpc, {
token: base,
amount: parseUnits('500', 6),
type: 'sell',
tick: Tick.fromPrice('1.001'),
})
// Sender has faucet alphaUsd (fee + swap source) but NO base tokens.
await Actions.fee.setUserToken(Relay.getClient({ account: sender }), {
token: Relay.addresses.alphaUsd,
})
// Sender attempts to transfer base via the wallet relay, which forwards
// to the app relay. The app relay returns 200 with capabilities.error =
// InsufficientBalance and a stub tx; the wallet relay must convert that
// into a synthetic throw so its own fill() autoSwap branch can recover.
const transferAmount = parseUnits('5', 6)
const result = await fillTransaction(client, {
account: sender.address,
...Actions.token.transfer.call(caller, {
token: base,
to: Relay.accounts[7]!.address,
amount: transferAmount,
}),
feePayer: appServer.url as never,
})
const { transaction, capabilities } = result
// Tx is filled with the swap calls prepended (approve + buy + transfer).
expect(transaction.calls).toHaveLength(3)
expect(transaction.feePayerSignature).toBeDefined()
// autoSwap metadata is surfaced.
expect(capabilities?.['autoSwap']?.slippage).toBe(0.05)
expect(capabilities?.['autoSwap']?.maxIn.symbol).toBe('AlphaUSD')
expect(capabilities?.['autoSwap']?.minOut.symbol).toBe('EXTBASE')
expect(capabilities?.['autoSwap']?.minOut.formatted).toBe('5')
})
},
)
describe.skipIf(Runtime.get().mode !== 'localnet')(
'behavior: app-provided feePayer URL bypasses wallet validate',
() => {
let appServer: Server
let walletServer: Server
let client: ReturnType
beforeAll(async () => {
// App relay is the authoritative sponsor — it has its own fee payer
// account and signs sponsored transactions.
appServer = await Relay.createServer(
(
await Relay.relay({
feePayer: {
account: feePayerAccount,
name: 'App Sponsor',
url: 'https://app.example.com',
},
})
).listener,
)
// Wallet relay has its own fee payer with a `validate` that ALWAYS
// rejects. This guards the wallet's own fee payer; it must NOT gate
// sponsorship when the dapp supplies its own external feePayer URL.
walletServer = await Relay.createServer(
(
await Relay.relay({
features: 'all',
internal_allowUnsafeUrls: true,
feePayer: {
account: feePayerAccount,
name: 'Wallet Sponsor',
validate: () => false,
},
})
).listener,
)
client = Relay.getClient({ url: walletServer.url })
})
afterAll(() => {
appServer.close()
walletServer.close()
})
test('behavior: external feePayer URL is sponsored even when wallet validate rejects', async () => {
const result = await fillTransaction(client, {
account: userAccount.address,
calls: [transferCall()],
feePayer: appServer.url as never,
})
expect(result.transaction.feePayerSignature).toBeDefined()
expect(result.transaction.maxFeePerGas).toBeDefined()
expect(result.transaction.maxFeePerGas).not.toBe(0n)
expect(result.capabilities?.['sponsored']).toBe(true)
expect(result.capabilities?.['sponsor']?.name).toBe('App Sponsor')
})
},
)
describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: chainId path parameter', () => {
let server: Server
let client: ReturnType
beforeAll(async () => {
server = await Relay.createServer((await Relay.relay({})).listener)
client = Relay.getClient({ url: `${server.url}/${Relay.chain.id}` })
})
afterAll(() => {
server.close()
})
test('default: proxies RPC methods via /:chainId path', async () => {
const chainId = await client.request({ method: 'eth_chainId' })
expect(Number(chainId)).toMatchInlineSnapshot(`${Relay.chain.id}`)
})
test('behavior: fills transaction via /:chainId path', async () => {
const { transaction } = await fillTransaction(client, {
account: userAccount.address,
calls: [transferCall()],
})
expect(transaction.gas).toBeDefined()
expect(transaction.nonce).toBeDefined()
})
test('behavior: handles batch requests via /:chainId path', async () => {
const response = await fetch(`${server.url}/${Relay.chain.id}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify([
{ jsonrpc: '2.0', id: 1, method: 'eth_chainId', params: [] },
{ jsonrpc: '2.0', id: 2, method: 'eth_chainId', params: [] },
]),
})
expect(response.status).toBe(200)
const body = (await response.json()) as { id: number; result: string }[]
expect(body).toHaveLength(2)
expect(Number(body[0]!.result)).toBe(Relay.chain.id)
expect(Number(body[1]!.result)).toBe(Relay.chain.id)
})
})
describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: capabilities', () => {
let server: Server
let client: ReturnType
beforeAll(async () => {
server = await Relay.createServer(
(
await Relay.relay({
features: 'all',
})
).listener,
)
client = Relay.getClient({ url: server.url })
})
afterAll(() => {
server.close()
})
test('default: returns fee and sponsored info', async () => {
const result = await fillTransaction(client, {
account: userAccount.address,
calls: [transferCall()],
})
const meta = result.capabilities
expect(meta?.['fee']).toBeDefined()
expect(meta?.['fee']?.decimals).toBe(6)
expect(meta?.['fee']?.symbol).toBe('AlphaUSD')
expect(meta?.['sponsored']).toBe(false)
})
test('behavior: token transfer produces balance diffs', async () => {
const sender = Relay.accounts[6]!
const recipient = Relay.accounts[7]!
const token = Relay.addresses.alphaUsd
// Sender is faucet-funded with alphaUsd (enough for transfer + fee).
// Set fee token so relay doesn't need pathUSD balance.
await Actions.fee.setUserToken(Relay.getClient(), { account: sender, token })
const { data, to: callTo } = Actions.token.transfer.call(caller, {
token,
to: recipient.address,
amount: 100n,
})
const result = await fillTransaction(client, {
account: sender.address,
to: callTo,
data,
})
const meta = result.capabilities
const senderDiffs = findDiffs(meta?.['balanceDiffs'], sender.address)!
const tokenDiff = senderDiffs.find((d) => d.address.toLowerCase() === token.toLowerCase())!
expect(tokenDiff.decimals).toBe(6)
expect(tokenDiff.direction).toBe('outgoing')
expect(tokenDiff.formatted).toBe('0.0001')
expect(tokenDiff.symbol).toBe('AlphaUSD')
expect(tokenDiff.value).toBe('0x64')
})
test('behavior: resolves direct virtual-address targets', async () => {
const virtualAddress = VirtualAddress.from({
masterId: '0xffffffff',
userTag: '0x000000000001',
})
const result = await fillTransaction(client, {
account: userAccount.address,
to: virtualAddress,
})
expect(virtualAddresses(result.capabilities)).toMatchInlineSnapshot(`
{
"0xfffffffffdfdfdfdfdfdfdfdfdfd000000000001": null,
}
`)
})
test('behavior: resolves TIP-20 memo transfer virtual-address recipients', async () => {
const virtualAddress = VirtualAddress.from({
masterId: '0xfffffffe',
userTag: '0x000000000002',
})
const result = await fillTransaction(client, {
account: userAccount.address,
calls: [
Actions.token.transfer.call(caller, {
amount: 1n,
memo: '0x01',
to: virtualAddress,
token: Relay.addresses.alphaUsd,
}),
],
capabilities: { errors: true },
})
expect(virtualAddresses(result.capabilities)).toMatchInlineSnapshot(`
{
"0xfffffffefdfdfdfdfdfdfdfdfdfd000000000002": null,
}
`)
})
test('behavior: approve + dex swap + transfer produces balance diffs', async () => {
const sender = Relay.accounts[8]!
const recipient = Relay.accounts[7]!
// Set up token pair + DEX liquidity.
const rpc = Relay.getClient({ account: Relay.accounts[0]! })
const { token: quote } = await Actions.token.createSync(rpc, {
name: 'Test Quote',
symbol: 'TQUOTE',
currency: 'USD',
})
const { token: base } = await Actions.token.createSync(rpc, {
name: 'Test Base',
symbol: 'TBASE',
currency: 'USD',
quoteToken: quote,
})
await sendTransactionSync(rpc, {
calls: [
Actions.token.grantRoles.call(caller, {
token: base,
role: 'issuer',
to: rpc.account!.address,
}),
Actions.token.grantRoles.call(caller, {
token: quote,
role: 'issuer',
to: rpc.account!.address,
}),
Actions.token.mint.call(caller, {
token: base,
to: rpc.account!.address,
amount: parseUnits('10000', 6),
}),
Actions.token.mint.call(caller, {
token: quote,
to: rpc.account!.address,
amount: parseUnits('10000', 6),
}),
Actions.token.approve.call(caller, {
token: base,
spender: Addresses.stablecoinDex,
amount: parseUnits('10000', 6),
}),
Actions.token.approve.call(caller, {
token: quote,
spender: Addresses.stablecoinDex,
amount: parseUnits('10000', 6),
}),
],
})
await Actions.dex.createPairSync(rpc, { base })
await Actions.dex.placeSync(rpc, {
token: base,
amount: parseUnits('500', 6),
type: 'sell',
tick: Tick.fromPrice('1.001'),
})
// Fund sender with quote tokens; alphaUsd fees come from the faucet.
await Actions.token.mintSync(rpc, {
token: quote,
amount: parseUnits('1000', 6),
to: sender.address,
})
await Actions.fee.setUserToken(Relay.getClient({ account: sender }), {
token: Relay.addresses.alphaUsd,
})
const buyAmount = parseUnits('10', 6)
const result = await fillTransaction(client, {
account: sender.address,
calls: [
Actions.token.approve.call(caller, {
token: quote,
spender: Addresses.stablecoinDex,
amount: parseUnits('100', 6),
}),
Actions.dex.buy.call({
tokenIn: quote,
tokenOut: base,
amountOut: buyAmount,
maxAmountIn: parseUnits('100', 6),
}),
Actions.token.transfer.call(caller, {
token: base,
to: recipient.address,
amount: buyAmount,
}),
],
})
const meta = result.capabilities
const diffs = findDiffs(meta?.['balanceDiffs'], sender.address)!
expect(diffs).toHaveLength(1)
const quoteDiff = diffs[0]!
expect(quoteDiff.address.toLowerCase()).toBe(quote.toLowerCase())
expect(quoteDiff.direction).toBe('outgoing')
expect(quoteDiff.symbol).toBe('TQUOTE')
expect(quoteDiff.name).toBe('Test Quote')
expect(quoteDiff.decimals).toBe(6)
// No base diff — bought and immediately transferred out (net zero).
expect(diffs.find((d) => d.address.toLowerCase() === base.toLowerCase())).toBeUndefined()
})
test('behavior: approval covered by transfer is suppressed', async () => {
const sender = Relay.accounts[6]!
const recipient = Relay.accounts[7]!
const token = Relay.addresses.alphaUsd
// approve(100) + transfer(100) to same spender → approval fully covered.
const result = await fillTransaction(client, {
account: sender.address,
calls: [
Actions.token.approve.call(caller, {
token,
spender: recipient.address,
amount: 100n,
}),
Actions.token.transfer.call(caller, {
token,
to: recipient.address,
amount: 100n,
}),
],
})
const meta = result.capabilities
const diffs = findDiffs(meta?.['balanceDiffs'], sender.address)!
const tokenDiff = diffs.find((d) => d.address.toLowerCase() === token.toLowerCase())!
// Only the transfer shows — approval is fully covered.
expect(tokenDiff.value).toBe('0x64')
expect(tokenDiff.direction).toBe('outgoing')
})
test('behavior: uncovered approval shows as outgoing', async () => {
const sender = Relay.accounts[6]!
const spender = Relay.accounts[7]!
const token = Relay.addresses.alphaUsd
// approve(200) + transfer(50) to same spender → 150 uncovered approval.
const result = await fillTransaction(client, {
account: sender.address,
calls: [
Actions.token.approve.call(caller, {
token,
spender: spender.address,
amount: 200n,
}),
Actions.token.transfer.call(caller, {
token,
to: spender.address,
amount: 50n,
}),
],
})
const meta = result.capabilities
const diffs = findDiffs(meta?.['balanceDiffs'], sender.address)!
const tokenDiff = diffs.find((d) => d.address.toLowerCase() === token.toLowerCase())!
// transfer(50) + uncovered approval(150) = 200 outgoing.
expect(tokenDiff.value).toBe('0xc8')
expect(tokenDiff.direction).toBe('outgoing')
})
})
describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: AMM resolution', () => {
let server: Server
let client: ReturnType
beforeAll(async () => {
server = await Relay.createServer(
(
await Relay.relay({
features: 'all',
})
).listener,
)
client = Relay.getClient({ url: server.url })
})
afterAll(() => {
server.close()
})
test('behavior: prepends swap calls on InsufficientBalance', async () => {
const sender = Relay.accounts[4]!
// Set up token pair + DEX liquidity.
// Use alphaUsd as the quote token so the relay can swap alphaUsd → base.
const rpc = Relay.getClient({ account: Relay.accounts[0]! })
const { token: base } = await Actions.token.createSync(rpc, {
name: 'Swap Base',
symbol: 'SWBASE',
currency: 'USD',
quoteToken: Relay.addresses.alphaUsd,
})
await sendTransactionSync(rpc, {
calls: [
Actions.token.grantRoles.call(caller, {
token: base,
role: 'issuer',
to: rpc.account!.address,
}),
Actions.token.mint.call(caller, {
token: base,
to: rpc.account!.address,
amount: parseUnits('10000', 6),
}),
Actions.token.approve.call(caller, {
token: base,
spender: Addresses.stablecoinDex,
amount: parseUnits('10000', 6),
}),
Actions.token.approve.call(caller, {
token: Relay.addresses.alphaUsd,
spender: Addresses.stablecoinDex,
amount: parseUnits('10000', 6),
}),
],
})
await Actions.dex.createPairSync(rpc, { base })
await Actions.dex.placeSync(rpc, {
token: base,
amount: parseUnits('500', 6),
type: 'sell',
tick: Tick.fromPrice('1.001'),
})
// Sender has faucet alphaUsd (fee token) but NO base tokens.
await Actions.fee.setUserToken(Relay.getClient({ account: sender }), {
token: Relay.addresses.alphaUsd,
})
// Sender tries to transfer base tokens they don't have.
// Relay should detect InsufficientBalance, swap alphaUsd → base via DEX, and retry.
const transferAmount = parseUnits('5', 6)
const result = await fillTransaction(client, {
account: sender.address,
...Actions.token.transfer.call(caller, {
token: base,
to: Relay.accounts[7]!.address,
amount: transferAmount,
}),
})
// Should succeed — relay auto-swapped quote → base.
const { transaction, capabilities } = result
expect(transaction.gas).toBeDefined()
expect(transaction.nonce).toBeDefined()
expect(transaction.feeToken).toBe(Relay.addresses.alphaUsd)
expect(transaction.calls).toHaveLength(3) // approve + swap + transfer
const m = capabilities
expect(m?.['sponsored']).toBe(false)
expect(m?.['fee']?.decimals).toBe(6)
expect(m?.['fee']?.symbol).toBe('AlphaUSD')
// Balance diffs exclude swap tokens — only the user's transfer shows.
const diffs = findDiffs(m?.['balanceDiffs'], sender.address)!
expect(diffs).toHaveLength(1)
expect(diffs[0]!.direction).toBe('outgoing')
expect(diffs[0]!.formatted).toBe('5')
expect(diffs[0]!.symbol).toBe('SWBASE')
expect(diffs[0]!.address.toLowerCase()).toBe(base.toLowerCase())
// autoSwap reports the injected AMM swap.
expect(m?.['autoSwap']?.slippage).toBe(0.05)
expect(m?.['autoSwap']?.maxIn.formatted).toBe('5.25')
expect(m?.['autoSwap']?.maxIn.symbol).toBe('AlphaUSD')
expect(m?.['autoSwap']?.maxIn.token.toLowerCase()).toBe(Relay.addresses.alphaUsd.toLowerCase())
expect(m?.['autoSwap']?.minOut.formatted).toBe('5')
expect(m?.['autoSwap']?.minOut.symbol).toBe('SWBASE')
expect(m?.['autoSwap']?.minOut.token.toLowerCase()).toBe(base.toLowerCase())
})
test('behavior: custom slippage is applied to autoSwap', async () => {
const sender = Relay.accounts[2]!
// Set up token pair + DEX liquidity.
const rpc = Relay.getClient({ account: Relay.accounts[0]! })
const { token: base } = await Actions.token.createSync(rpc, {
name: 'Slippage Base',
symbol: 'SLPBASE',
currency: 'USD',
quoteToken: Relay.addresses.alphaUsd,
})
await sendTransactionSync(rpc, {
calls: [
Actions.token.grantRoles.call(caller, {
token: base,
role: 'issuer',
to: rpc.account!.address,
}),
Actions.token.mint.call(caller, {
token: base,
to: rpc.account!.address,
amount: parseUnits('10000', 6),
}),
Actions.token.approve.call(caller, {
token: base,
spender: Addresses.stablecoinDex,
amount: parseUnits('10000', 6),
}),
Actions.token.approve.call(caller, {
token: Relay.addresses.alphaUsd,
spender: Addresses.stablecoinDex,
amount: parseUnits('10000', 6),
}),
],
})
await Actions.dex.createPairSync(rpc, { base })
await Actions.dex.placeSync(rpc, {
token: base,
amount: parseUnits('500', 6),
type: 'sell',
tick: Tick.fromPrice('1.001'),
})
// Sender has faucet alphaUsd but NO base tokens.
await Actions.fee.setUserToken(Relay.getClient({ account: sender }), {
token: Relay.addresses.alphaUsd,
})
// Create relay with custom 2% slippage.
const customServer = await Relay.createServer(
(
await Relay.relay({
features: 'all',
autoSwap: { slippage: 0.02 },
})
).listener,
)
const customClient = Relay.getClient({ url: customServer.url })
const result = await fillTransaction(customClient, {
account: sender.address,
...Actions.token.transfer.call(caller, {
token: base,
to: Relay.accounts[7]!.address,
amount: parseUnits('10', 6),
}),
})
customServer.close()
const m = result.capabilities
expect(m?.['autoSwap']?.slippage).toBe(0.02)
// 10 + 2% = 10.2
expect(m?.['autoSwap']?.maxIn.formatted).toBe('10.2')
expect(m?.['autoSwap']?.minOut.formatted).toBe('10')
})
test('behavior: autoSwap disabled throws InsufficientBalance instead of swapping', async () => {
const sender = Relay.accounts[3]!
// Set up token pair + DEX liquidity.
const rpc = Relay.getClient({ account: Relay.accounts[0]! })
const { token: base } = await Actions.token.createSync(rpc, {
name: 'No Swap Base',
symbol: 'NSWBASE',
currency: 'USD',
quoteToken: Relay.addresses.alphaUsd,
})
await sendTransactionSync(rpc, {
calls: [
Actions.token.grantRoles.call(caller, {
token: base,
role: 'issuer',
to: rpc.account!.address,
}),
Actions.token.mint.call(caller, {
token: base,
to: rpc.account!.address,
amount: parseUnits('10000', 6),
}),
Actions.token.approve.call(caller, {
token: base,
spender: Addresses.stablecoinDex,
amount: parseUnits('10000', 6),
}),
],
})
await Actions.dex.createPairSync(rpc, { base })
await Actions.dex.placeSync(rpc, {
token: base,
amount: parseUnits('500', 6),
type: 'sell',
tick: Tick.fromPrice('1.001'),
})
// Sender has faucet alphaUsd but NO base tokens.
await Actions.fee.setUserToken(Relay.getClient({ account: sender }), {
token: Relay.addresses.alphaUsd,
})
// Create relay with autoSwap disabled.
const customServer = await Relay.createServer(
(
await Relay.relay({
features: 'all',
autoSwap: false,
})
).listener,
)
const customClient = Relay.getClient({ url: customServer.url })
// Should return error capability instead of auto-swapping.
const result = await fillTransaction(customClient, {
account: sender.address,
calls: [
Actions.token.transfer.call(caller, {
token: base,
to: Relay.accounts[7]!.address,
amount: parseUnits('5', 6),
}),
],
capabilities: { errors: true },
})
const error = result.capabilities?.['error']
expect({ ...error, data: undefined }).toMatchInlineSnapshot(`
{
"abiItem": {
"inputs": [
{
"name": "available",
"type": "uint256",
},
{
"name": "required",
"type": "uint256",
},
{
"name": "token",
"type": "address",
},
],
"name": "InsufficientBalance",
"type": "error",
},
"data": undefined,
"errorName": "InsufficientBalance",
"message": "Insufficient balance. Required: 5000000, available: 0.",
}
`)
customServer.close()
})
})
describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: conditional sponsoring', () => {
let server: Server
let client: ReturnType
beforeAll(async () => {
// accounts[3] is faucet-funded with alphaUsd so transfers succeed.
await Actions.fee.setUserToken(Relay.getClient(), {
account: Relay.accounts[3]!,
token: Relay.addresses.alphaUsd,
})
server = await Relay.createServer(
(
await Relay.relay({
feePayer: {
account: feePayerAccount,
name: 'Test Sponsor',
url: 'https://test.com',
validate: (request) =>
request.from?.toLowerCase() !== Relay.accounts[3]!.address.toLowerCase(),
},
})
).listener,
)
client = Relay.getClient({ url: server.url })
})
afterAll(() => {
server.close()
})
test('behavior: approved tx is sponsored and can be broadcast', async () => {
const { transaction } = await fillTransaction(client, {
account: userAccount.address,
calls: [transferCall()],
})
expect(transaction.feePayerSignature).toBeDefined()
const signed = await userAccount.signTransaction(transaction as never)
const receipt = (await Relay.getClient().request({
method: 'eth_sendRawTransactionSync' as never,
params: [signed],
})) as { feePayer?: string | undefined }
expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
})
test('behavior: rejected tx is not sponsored and can be self-paid', async () => {
const sender = Relay.accounts[3]!
const result = await fillTransaction(client, {
account: sender.address,
calls: [transferCall()],
})
expect(result.transaction.feePayerSignature).toBeUndefined()
const meta = result.capabilities
expect(meta?.['sponsored']).toBe(false)
expect(meta?.['sponsor']).toBeUndefined()
const serialized = (await Transaction.serialize(result.transaction as never)) as `0x76${string}`
const envelope = TxEnvelopeTempo.deserialize(serialized)
const signature = await sender.sign({
hash: TxEnvelopeTempo.getSignPayload(envelope),
})
const signed = TxEnvelopeTempo.serialize(envelope, {
signature: SignatureEnvelope.from(signature),
})
const receipt = (await Relay.getClient().request({
method: 'eth_sendRawTransactionSync' as never,
params: [signed],
})) as { feePayer?: string | undefined }
// Sender pays their own fee — no external fee payer.
expect(receipt.feePayer).not.toBe(feePayerAccount.address.toLowerCase())
})
})
describe.skipIf(Runtime.get().mode !== 'localnet')(
'behavior: path A — guaranteed sponsorship (no validate)',
() => {
let server: Server
let client: ReturnType
let requests: RpcRequest.RpcRequest[] = []
beforeAll(async () => {
server = await Relay.createServer(
(
await Relay.relay({
features: 'all',
tokens: localnetTokens,
feePayer: {
account: feePayerAccount,
name: 'Path A Sponsor',
},
onRequest: async (request) => {
requests.push(request)
},
})
).listener,
)
client = Relay.getClient({ url: server.url })
})
afterAll(() => {
server.close()
})
afterEach(() => {
requests = []
})
test('behavior: skips fee token resolution and sponsors', async () => {
const result = await fillTransaction(client, {
account: userAccount.address,
calls: [transferCall()],
})
expect(result.transaction.feePayerSignature).toBeDefined()
expect(result.capabilities?.['sponsored']).toBe(true)
expect(result.capabilities?.['sponsor']?.name).toBe('Path A Sponsor')
// Only one fill request — no fee token resolution round-trip.
expect(requests.filter((r) => r.method === 'eth_fillTransaction')).toHaveLength(1)
})
test('behavior: returns fee even when no feeToken in request', async () => {
const result = await fillTransaction(client, {
account: userAccount.address,
calls: [transferCall()],
})
expect(result.capabilities?.['fee']).toBeDefined()
expect(result.capabilities?.['fee']?.decimals).toBeTypeOf('number')
expect(result.capabilities?.['fee']?.symbol).toBeTypeOf('string')
expect(result.capabilities?.['fee']?.formatted).toBeTypeOf('string')
})
test('behavior: simulate and sign run concurrently with fill', async () => {
const result = await fillTransaction(client, {
account: userAccount.address,
calls: [transferCall()],
})
// All capabilities are present despite parallel execution.
expect(result.transaction.feePayerSignature).toBeDefined()
expect(result.capabilities?.['sponsored']).toBe(true)
})
test('behavior: defaults feeToken to chain default when caller omits it', async () => {
// Without the default, the broadcast envelope has no feeToken and
// the chain falls back to the sender's account token, which often
// lacks FeeAMM liquidity.
const result = await fillTransaction(client, {
account: userAccount.address,
calls: [transferCall()],
})
expect(result.transaction.feeToken?.toLowerCase()).toBe(
localnetTokens[0]!.address.toLowerCase(),
)
})
test('behavior: preserves caller-supplied feeToken', async () => {
const result = await fillTransaction(client, {
account: userAccount.address,
calls: [transferCall()],
feeToken: Relay.addresses.alphaUsd as Address,
})
expect(result.transaction.feeToken?.toLowerCase()).toBe(
Relay.addresses.alphaUsd.toLowerCase(),
)
})
},
)
describe.skipIf(Runtime.get().mode !== 'localnet')(
'behavior: path B — conditional sponsorship (validate)',
() => {
let server: Server
let client: ReturnType
let requests: RpcRequest.RpcRequest[] = []
// Reject accounts[3], approve everyone else.
const rejectedSender = Relay.accounts[3]!
beforeAll(async () => {
// rejectedSender is faucet-funded with alphaUsd so it can self-pay.
await Actions.fee.setUserToken(Relay.getClient(), {
account: rejectedSender,
token: Relay.addresses.alphaUsd,
})
server = await Relay.createServer(
(
await Relay.relay({
features: 'all',
feePayer: {
account: feePayerAccount,
name: 'Path B Sponsor',
validate: (request) =>
request.from?.toLowerCase() !== rejectedSender.address.toLowerCase(),
},
onRequest: async (request) => {
requests.push(request)
},
})
).listener,
)
client = Relay.getClient({ url: server.url })
})
afterAll(() => {
server.close()
})
afterEach(() => {
requests = []
})
test('behavior: approved sender gets sponsorship with parallel fills', async () => {
const result = await fillTransaction(client, {
account: userAccount.address,
calls: [transferCall()],
})
expect(result.transaction.feePayerSignature).toBeDefined()
expect(result.capabilities?.['sponsored']).toBe(true)
expect(result.capabilities?.['sponsor']?.name).toBe('Path B Sponsor')
})
test('behavior: rejected sender gets unsponsored tx from parallel fill', async () => {
const result = await fillTransaction(client, {
account: rejectedSender.address,
calls: [transferCall()],
})
expect(result.transaction.feePayerSignature).toBeUndefined()
expect(result.capabilities?.['sponsored']).toBe(false)
expect(result.capabilities?.['sponsor']).toBeUndefined()
})
test('behavior: rejected tx can be signed and broadcast by sender', async () => {
const result = await fillTransaction(client, {
account: rejectedSender.address,
calls: [transferCall()],
})
const serialized = (await Transaction.serialize(
result.transaction as never,
)) as `0x76${string}`
const envelope = TxEnvelopeTempo.deserialize(serialized)
const signature = await rejectedSender.sign({
hash: TxEnvelopeTempo.getSignPayload(envelope),
})
const signed = TxEnvelopeTempo.serialize(envelope, {
signature: SignatureEnvelope.from(signature),
})
const receipt = (await Relay.getClient().request({
method: 'eth_sendRawTransactionSync' as never,
params: [signed],
})) as { feePayer?: string | undefined }
expect(receipt.feePayer).not.toBe(feePayerAccount.address.toLowerCase())
})
},
)
describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: path C — no sponsorship', () => {
let server: Server
let client: ReturnType
let requests: RpcRequest.RpcRequest[] = []
beforeAll(async () => {
server = await Relay.createServer(
(
await Relay.relay({
features: 'all',
onRequest: async (request) => {
requests.push(request)
},
})
).listener,
)
client = Relay.getClient({ url: server.url })
})
afterAll(() => {
server.close()
})
afterEach(() => {
requests = []
})
test('behavior: resolves fee token and fills without sponsorship', async () => {
const result = await fillTransaction(client, {
account: userAccount.address,
calls: [transferCall()],
})
expect(result.transaction.feePayerSignature).toBeUndefined()
expect(result.capabilities?.['sponsored']).toBe(false)
expect(result.capabilities?.['fee']).toBeDefined()
// Single fill — no speculative second fill.
expect(requests.filter((r) => r.method === 'eth_fillTransaction')).toHaveLength(1)
})
test('behavior: simulate and autoSwap metadata resolve concurrently', async () => {
const result = await fillTransaction(client, {
account: userAccount.address,
calls: [transferCall()],
})
// Capabilities are populated despite parallel execution.
expect(result.transaction.gas).toBeDefined()
expect(result.transaction.nonce).toBeDefined()
expect(result.capabilities?.['fee']).toBeDefined()
expect(result.capabilities?.['sponsored']).toBe(false)
})
})
describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: fee token resolution', () => {
const feeTokenAccount = Relay.accounts[0]!
const preferredToken = Relay.addresses.alphaUsd
let server: Server
let client: ReturnType
beforeAll(async () => {
// feeTokenAccount is faucet-funded with alphaUsd so the balance check passes.
// Set on-chain fee token preference.
await Actions.fee.setUserToken(Relay.getClient(), {
account: feeTokenAccount,
token: preferredToken,
})
server = await Relay.createServer(
(
await Relay.relay({
features: 'all',
tokens: localnetTokens,
})
).listener,
)
client = Relay.getClient({ url: server.url })
})
afterAll(() => {
server.close()
})
test('behavior: uses explicitly provided feeToken', async () => {
const feeToken = '0x20c0000000000000000000000000000000000001'
const { transaction } = await fillTransaction(client, {
account: feeTokenAccount.address,
calls: [transferCall()],
feeToken,
})
expect(transaction.feeToken).toBe(feeToken)
})
test('behavior: resolves to onchain user token when it has balance', async () => {
const { transaction } = await fillTransaction(client, {
account: feeTokenAccount.address,
calls: [transferCall()],
})
expect(transaction.feeToken).toBe(preferredToken)
})
test('behavior: resolves to highest-balance token from token list', async () => {
// Create two fresh TIP-20s and mint different amounts to an account that is
// NOT faucet-funded, so only these two balances influence resolution
// (a faucet-funded account would carry genesis balances that dominate).
// The relay's tokenlist is restricted to just these two so the
// highest-balance resolver picks between them deterministically.
const freshAccount = Relay.accounts[5]!
const rpc = Relay.getClient({ account: Relay.accounts[0]! })
const { token: lowUsd } = await Actions.token.createSync(rpc, {
name: 'LowUSD',
symbol: 'LowUSD',
currency: 'USD',
quoteToken: Relay.addresses.alphaUsd,
})
const { token: highUsd } = await Actions.token.createSync(rpc, {
name: 'HighUSD',
symbol: 'HighUSD',
currency: 'USD',
quoteToken: Relay.addresses.alphaUsd,
})
await sendTransactionSync(rpc, {
calls: [
Actions.token.grantRoles.call(caller, {
token: lowUsd,
role: 'issuer',
to: rpc.account!.address,
}),
Actions.token.grantRoles.call(caller, {
token: highUsd,
role: 'issuer',
to: rpc.account!.address,
}),
Actions.token.mint.call(caller, {
token: lowUsd,
amount: parseUnits('100', 6),
to: freshAccount.address,
}),
Actions.token.mint.call(caller, {
token: highUsd,
amount: parseUnits('500', 6),
to: freshAccount.address,
}),
],
})
const customServer = await Relay.createServer(
(
await Relay.relay({
features: 'all',
tokens: [
{ address: lowUsd, decimals: 6, name: 'LowUSD', symbol: 'LowUSD' },
{ address: highUsd, decimals: 6, name: 'HighUSD', symbol: 'HighUSD' },
],
})
).listener,
)
const customClient = Relay.getClient({ url: customServer.url })
const { transaction } = await fillTransaction(customClient, {
account: freshAccount.address,
calls: [
Actions.token.transfer.call(caller, { token: highUsd, to: recipient.address, amount: 1n }),
],
})
customServer.close()
// highUsd has the higher balance (500 > 100).
expect(transaction.feeToken?.toLowerCase()).toBe(highUsd.toLowerCase())
})
test('behavior: falls back to pathUSD when no preference or balances', async () => {
const freshAccount = Relay.accounts[10]!
const { transaction } = await fillTransaction(client, {
account: freshAccount.address,
calls: [
Actions.token.transfer.call(caller, {
token: Relay.addresses.alphaUsd,
to: freshAccount.address,
amount: 0n,
}),
],
})
expect(transaction.feeToken).toBeUndefined()
})
})
describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: error capabilities', () => {
let server: Server
let client: ReturnType
beforeAll(async () => {
server = await Relay.createServer(
(
await Relay.relay({
features: 'all',
getClient: Relay.readClient('https://rpc.moderato.tempo.xyz', tempoModerato.id),
})
).listener,
)
client = Relay.getClient({ url: server.url })
})
afterAll(() => {
server.close()
})
// Skipped: this asserts an inline snapshot of the sender's live moderato
// `balanceDiffs`, which depends on that account's balances on the hosted
// moderato testnet at capture time — not reproducible in the disposable
// localnet harness. The sibling error/JSON-RPC error tests below cover the
// same relay error-capability behavior without a live-balance snapshot.
test.skip('behavior: returns requireFunds on InsufficientBalance when errors capability is enabled', async () => {
const sender = Relay.accounts[10]!
const result = await fillTransaction(client, {
account: sender.address,
calls: [
Actions.token.transfer.call(caller, {
token: Relay.addresses.alphaUsd,
to: recipient.address,
amount: parseUnits('100', 6),
}),
],
capabilities: { errors: true },
})
expect(result.capabilities).toMatchInlineSnapshot(`
{
"balanceDiffs": {
"0x0eB552e73e6f8E0922749e0fB08af2a71ECb2b7F": [
{
"address": "0x20c0000000000000000000000000000000000001",
"decimals": 6,
"direction": "outgoing",
"formatted": "100",
"name": "AlphaUSD",
"recipients": [
"0xAF4311d557fBC876059e39306ec1f3343753df29",
],
"symbol": "AlphaUSD",
"value": "0x5f5e100",
},
],
},
"error": {
"abiItem": {
"inputs": [
{
"name": "available",
"type": "uint256",
},
{
"name": "required",
"type": "uint256",
},
{
"name": "token",
"type": "address",
},
],
"name": "InsufficientBalance",
"type": "error",
},
"data": "0x832f98b500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005f5e10000000000000000000000000020c0000000000000000000000000000000000001",
"errorName": "InsufficientBalance",
"message": "Insufficient balance. Required: 100000000, available: 0.",
},
"requireFunds": {
"amount": "0x5f5e100",
"decimals": 6,
"formatted": "100",
"symbol": "AlphaUSD",
"token": "0x20C0000000000000000000000000000000000001",
},
"sponsored": false,
}
`)
})
test('behavior: returns error capability on generic revert when errors capability is enabled', async () => {
const sender = Relay.accounts[10]!
const result = await fillTransaction(client, {
account: sender.address,
calls: [
Actions.token.grantRoles.call(caller, {
token: Relay.addresses.alphaUsd,
role: 'issuer',
to: sender.address,
}),
],
capabilities: { errors: true },
})
expect(result.capabilities).toMatchInlineSnapshot(`
{
"error": {
"abiItem": {
"inputs": [],
"name": "Unauthorized",
"type": "error",
},
"data": "0x82b42900",
"errorName": "Unauthorized",
"message": "Unauthorized.",
},
"sponsored": false,
}
`)
})
test('default: throws JSON-RPC error on InsufficientBalance', async () => {
const sender = Relay.accounts[10]!
const error = await fillTransaction(client, {
account: sender.address,
calls: [
Actions.token.transfer.call(caller, {
token: Relay.addresses.alphaUsd,
to: recipient.address,
amount: parseUnits('100', 6),
}),
],
}).then(
() => undefined,
(e: BaseError) => e,
)
expect(error).toBeDefined()
// Walk the viem error chain to the underlying RPC error. The default path
// surfaces the chain revert as a JSON-RPC error (`code: 3`) with the
// ABI-encoded `InsufficientBalance(uint256, uint256, address)` selector.
const rpc = error?.walk(isRpcError) as { data?: string } | undefined
expect(rpc?.data?.startsWith('0x832f98b5')).toBe(true)
})
test('default: throws JSON-RPC error on generic revert', async () => {
const sender = Relay.accounts[10]!
const error = await fillTransaction(client, {
account: sender.address,
calls: [
Actions.token.grantRoles.call(caller, {
token: Relay.addresses.alphaUsd,
role: 'issuer',
to: sender.address,
}),
],
}).then(
() => undefined,
(e: BaseError) => e,
)
expect(error).toBeDefined()
const rpc = error?.walk(isRpcError) as { data?: string } | undefined
// ABI-encoded `Unauthorized()`.
expect(rpc?.data).toBe('0x82b42900')
})
test('behavior: explicit `errors: false` matches default JSON-RPC error behavior', async () => {
const sender = Relay.accounts[10]!
const error = await fillTransaction(client, {
account: sender.address,
calls: [
Actions.token.transfer.call(caller, {
token: Relay.addresses.alphaUsd,
to: recipient.address,
amount: parseUnits('100', 6),
}),
],
capabilities: { errors: false } as never,
}).then(
() => undefined,
(e: BaseError) => e,
)
expect(error).toBeDefined()
const rpc = error?.walk(isRpcError) as { data?: string } | undefined
expect(rpc?.data?.startsWith('0x832f98b5')).toBe(true)
})
})
function isRpcError(e: unknown): boolean {
return typeof e === 'object' && e !== null && 'code' in e && (e as { code: unknown }).code === 3
}
// Skipped: this drives a real mainnet fill against the hosted presto RPC using
// a hard-coded sender that must hold live USDC.e (and no PathUSD) for autoSwap
// to trigger — external state that isn't reproducible in the localnet harness.
describe.skip('behavior: mainnet autoSwap with USDC.e → PathUSD', () => {
let server: Server
let mainnetClient: ReturnType
const mainnetUsdce = '0x20c000000000000000000000b9537d11c60e8b50' as const
const mainnetPathUsd = '0x20c0000000000000000000000000000000000000' as const
const mainnetSender = '0xb472f3ca15f34Db22d43FA503043F1e6541AC085' as const
beforeAll(async () => {
server = await Relay.createServer(
(
await Relay.relay({
features: 'all',
getClient: Relay.readClient('https://rpc.presto.tempo.xyz', tempo.id),
})
).listener,
)
mainnetClient = Relay.getClient({ url: server.url })
})
afterAll(() => {
server.close()
})
test('behavior: auto-swaps USDC.e → PathUSD when sender has USDC.e but no PathUSD', async () => {
const result = await fillTransaction(mainnetClient, {
account: mainnetSender,
calls: [
Actions.token.transfer.call(caller, {
token: mainnetPathUsd,
to: recipient.address,
amount: parseUnits('0.5', 6),
}),
],
})
// Expected: relay detects InsufficientBalance for PathUSD, auto-swaps
// USDC.e → PathUSD via DEX, and returns a filled transaction with swap calls.
const { transaction, capabilities } = result
expect(transaction.gas).toBeDefined()
expect(transaction.nonce).toBeDefined()
expect(transaction.calls!.length).toBeGreaterThanOrEqual(3) // approve + swap + transfer
expect(capabilities?.['autoSwap']).toBeDefined()
expect(capabilities?.['autoSwap']?.maxIn.token.toLowerCase()).toBe(mainnetUsdce.toLowerCase())
expect(capabilities?.['autoSwap']?.minOut.token.toLowerCase()).toBe(
mainnetPathUsd.toLowerCase(),
)
expect(capabilities?.['sponsored']).toBe(false)
// Should NOT have requireFunds — autoSwap should handle it.
expect((capabilities as Record)?.['requireFunds']).toBeUndefined()
})
})