/** @module-tag localnet */
import type { RpcRequest } from 'ox'
import { SignatureEnvelope, TxEnvelopeTempo } from 'ox/tempo'
import { type Address, type BaseError, createClient, parseUnits, toHex } from 'viem'
import { generatePrivateKey } from 'viem/accounts'
import {
fillTransaction,
getTransaction,
prepareTransactionRequest,
sendTransactionSync,
} from 'viem/actions'
import {
Account,
Actions,
Addresses,
Capabilities,
Tick,
Transaction,
VirtualAddress,
withRelay,
} from 'viem/tempo'
import { tempo, tempoModerato } from 'viem/tempo/chains'
import * as Relay from '../../test/Relay.js'
import * as Runtime from '../../test/runtime.js'
import * as Store from '../internal/Store.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.runIf(Runtime.get().mode === 'localnet' && process.env.TEMPO_IMAGE_TAG === 'sha-2bd9fa3')(
'multisig',
() => {
let client: ReturnType
let server: Server
const store = Store.memory()
beforeAll(async () => {
server = await Relay.createServer(
(
await Relay.relay({
multisig: { store },
tokens: localnetTokens,
})
).listener,
)
client = createClient({
chain: Relay.chain,
pollingInterval: 100,
transport: withRelay(Relay.http(), Relay.http(server.url)),
})
})
afterAll(() => {
server.close()
})
test('example: initial configuration', async () => {
const owner_1 = Relay.accounts[1]!
const owner_2 = Relay.accounts[2]!
const account = Account.fromMultisig({
address: 'infer',
owners: [owner_1.address, owner_2.address],
salt: toHex(0x109700, { size: 32 }),
threshold: 2,
})
await Actions.token.transferSync(caller, {
account: feePayerAccount,
amount: parseUnits('1', 6),
to: account.address,
token: Relay.addresses.alphaUsd,
})
const balance = await Actions.token.getBalance(client, {
account: recipient.address,
token: Relay.addresses.alphaUsd,
})
const { receipt: pending } = await Actions.token.transferSync(client, {
account,
amount: 1n,
owner: owner_1,
to: recipient.address,
token: Relay.addresses.alphaUsd,
})
expect({
signatureCount: pending.multisig?.signatureCount,
status: pending.status,
threshold: pending.multisig?.threshold,
weight: pending.multisig?.weight,
}).toMatchInlineSnapshot(`
{
"signatureCount": 1,
"status": "pending",
"threshold": 2,
"weight": 1,
}
`)
const { receipt } = await Actions.token.transferSync(client, {
account,
amount: 1n,
hash: pending.transactionHash,
owner: owner_2,
to: recipient.address,
token: Relay.addresses.alphaUsd,
} as never)
expect({
signatureCount: receipt.multisig?.signatureCount,
status: receipt.status,
threshold: receipt.multisig?.threshold,
weight: receipt.multisig?.weight,
}).toMatchInlineSnapshot(`
{
"signatureCount": 2,
"status": "success",
"threshold": 2,
"weight": 2,
}
`)
expect(
(
await Actions.token.getBalance(client, {
account: recipient.address,
token: Relay.addresses.alphaUsd,
})
).amount - balance.amount,
).toMatchInlineSnapshot(`1n`)
const transaction = await getTransaction(client, {
hash: receipt.transactionHash,
})
if (transaction.signature?.type !== 'multisig')
throw new Error('Expected a multisig signature.')
expect({
account: transaction.signature.account,
signatureCount: transaction.signature.signatures.length,
type: transaction.signature.type,
version: transaction.signature.config.version,
}).toMatchInlineSnapshot(`
{
"account": "${account.address.toLowerCase()}",
"signatureCount": 2,
"type": "multisig",
"version": 0n,
}
`)
})
test('example: nested ownership', async () => {
const childOwner = Relay.accounts[3]!
const child = Account.fromMultisig({
address: 'infer',
owners: [childOwner],
salt: toHex(0x109701, { size: 32 }),
})
await Actions.token.transferSync(caller, {
account: feePayerAccount,
amount: parseUnits('1', 6),
to: child.address,
token: Relay.addresses.alphaUsd,
})
const current = await Actions.multisig.updateConfigSync(client, {
account: child,
nextConfig: {
owners: [{ owner: childOwner.address, weight: 1 }],
threshold: 1,
},
owner: childOwner,
})
expect(current.receipt.status).toMatchInlineSnapshot(`"success"`)
const currentChild = Account.fromMultisig({
address: child.address,
owners: [{ owner: childOwner, weight: 1 }],
salt: current.config.salt,
threshold: current.config.threshold,
version: current.config.version,
})
const parent = Account.fromMultisig({
address: 'infer',
owners: [currentChild],
salt: toHex(0x109702, { size: 32 }),
})
await Actions.token.transferSync(caller, {
account: feePayerAccount,
amount: parseUnits('1', 6),
to: parent.address,
token: Relay.addresses.alphaUsd,
})
const currentParent = await Actions.multisig.updateConfigSync(client, {
account: parent,
nextConfig: {
owners: [{ owner: currentChild.address, weight: 1 }],
threshold: 1,
},
owner: currentChild,
})
expect(currentParent.receipt.status).toMatchInlineSnapshot(`"success"`)
const account = Account.fromMultisig({
address: parent.address,
owners: [{ owner: currentChild, weight: 1 }],
salt: currentParent.config.salt,
threshold: currentParent.config.threshold,
version: currentParent.config.version,
})
const { receipt } = await Actions.token.transferSync(client, {
account,
amount: 2n,
owner: currentChild,
to: recipient.address,
token: Relay.addresses.alphaUsd,
})
expect(receipt.status).toMatchInlineSnapshot(`"success"`)
const transaction = await getTransaction(client, {
hash: receipt.transactionHash,
})
expect(transaction.signature?.type).toMatchInlineSnapshot(`"multisig"`)
if (transaction.signature?.type !== 'multisig')
throw new Error('Expected a multisig signature.')
expect(transaction.signature.signatures.map((signature) => signature.type))
.toMatchInlineSnapshot(`
[
"multisig",
]
`)
})
test('example: weighted quorum', async () => {
const owners = [Relay.accounts[4]!, Relay.accounts[6]!, Relay.accounts[8]!].sort((a, b) =>
a.address.localeCompare(b.address),
)
const heavy = owners[0]!
const light_1 = owners[1]!
const light_2 = owners[2]!
const account = Account.fromMultisig({
address: 'infer',
owners: [
{ owner: heavy.address, weight: 2 },
{ owner: light_1.address, weight: 1 },
{ owner: light_2.address, weight: 1 },
],
salt: toHex(0x109703, { size: 32 }),
threshold: 3,
})
await Actions.token.transferSync(caller, {
account: feePayerAccount,
amount: parseUnits('1', 6),
to: account.address,
token: Relay.addresses.alphaUsd,
})
const { receipt: alicePending } = await Actions.token.transferSync(client, {
account,
amount: 3n,
owner: heavy,
to: recipient.address,
token: Relay.addresses.alphaUsd,
})
expect({
status: alicePending.status,
weight: alicePending.multisig?.weight,
}).toMatchInlineSnapshot(`
{
"status": "pending",
"weight": 2,
}
`)
const { receipt: aliceBob } = await Actions.token.transferSync(client, {
account,
amount: 3n,
hash: alicePending.transactionHash,
owner: light_1,
to: recipient.address,
token: Relay.addresses.alphaUsd,
} as never)
expect(aliceBob.status).toMatchInlineSnapshot(`"success"`)
const { receipt: secondAlicePending } = await Actions.token.transferSync(client, {
account,
amount: 4n,
owner: heavy,
to: recipient.address,
token: Relay.addresses.alphaUsd,
})
const { receipt: aliceCarol } = await Actions.token.transferSync(client, {
account,
amount: 4n,
hash: secondAlicePending.transactionHash,
owner: light_2,
to: recipient.address,
token: Relay.addresses.alphaUsd,
} as never)
expect(aliceCarol.status).toMatchInlineSnapshot(`"success"`)
const { receipt: bobPending } = await Actions.token.transferSync(client, {
account,
amount: 5n,
owner: light_1,
to: recipient.address,
token: Relay.addresses.alphaUsd,
})
const { receipt: bobCarolPending } = await Actions.token.transferSync(client, {
account,
amount: 5n,
hash: bobPending.transactionHash,
owner: light_2,
to: recipient.address,
token: Relay.addresses.alphaUsd,
} as never)
expect({
status: bobCarolPending.status,
weight: bobCarolPending.multisig?.weight,
}).toMatchInlineSnapshot(`
{
"status": "pending",
"weight": 2,
}
`)
const { receipt } = await Actions.token.transferSync(client, {
account,
amount: 5n,
hash: bobCarolPending.transactionHash,
owner: heavy,
to: recipient.address,
token: Relay.addresses.alphaUsd,
} as never)
expect(receipt.status).toMatchInlineSnapshot(`"success"`)
const transaction = await getTransaction(client, {
hash: receipt.transactionHash,
})
if (transaction.signature?.type !== 'multisig')
throw new Error('Expected a multisig signature.')
expect({
signatureCount: transaction.signature.signatures.length,
status: receipt.status,
weight: receipt.multisig?.weight,
}).toMatchInlineSnapshot(`
{
"signatureCount": 2,
"status": "success",
"weight": 3,
}
`)
})
test('example: fee sponsorship', async () => {
const sponsorStore = Store.memory()
const sponsorServer = await Relay.createServer(
(
await Relay.relay({
feePayer: {
account: feePayerAccount,
onSponsored: () => ({ subsidized: true }),
},
multisig: { store: sponsorStore },
tokens: localnetTokens,
})
).listener,
)
const sponsorClient = createClient({
chain: Relay.chain,
pollingInterval: 100,
transport: withRelay(Relay.http(), Relay.http(sponsorServer.url)),
})
const owner_1 = Relay.accounts[1]!
const owner_2 = Relay.accounts[2]!
try {
const transaction = await prepareTransactionRequest(caller, {
account: userAccount.address,
calls: [transferCall()],
feePayer: true,
})
const serialized = await userAccount.signTransaction(transaction as never)
const response = await fetch(sponsorServer.url, {
body: JSON.stringify({
id: 1,
jsonrpc: '2.0',
method: 'eth_sendRawTransactionSync',
params: [serialized],
}),
headers: { 'content-type': 'application/json' },
method: 'POST',
})
expect(await response.json()).toMatchObject({
sponsorship_details: { subsidized: true },
})
const ownerFirst = Account.fromMultisig({
address: 'infer',
owners: [owner_1.address, owner_2.address],
salt: toHex(0x109704, { size: 32 }),
threshold: 2,
})
await Actions.token.transferSync(caller, {
account: feePayerAccount,
amount: parseUnits('1', 6),
to: ownerFirst.address,
token: Relay.addresses.alphaUsd,
})
const { receipt: ownerFirstPending } = await Actions.token.transferSync(sponsorClient, {
account: ownerFirst,
amount: 6n,
feePayer: true,
owner: owner_1,
to: recipient.address,
token: Relay.addresses.alphaUsd,
})
if (!ownerFirstPending.multisig) throw new Error('Expected a multisig operation.')
const ownerFirstTransaction = Transaction.deserialize(
ownerFirstPending.multisig.transaction,
)
expect({
feePayerSigned:
'feePayerSignature' in ownerFirstTransaction &&
!!ownerFirstTransaction.feePayerSignature,
status: ownerFirstPending.status,
}).toMatchInlineSnapshot(`
{
"feePayerSigned": false,
"status": "pending",
}
`)
const { receipt: ownerFirstReceipt } = await Actions.token.transferSync(sponsorClient, {
account: ownerFirst,
amount: 6n,
hash: ownerFirstPending.transactionHash,
owner: owner_2,
to: recipient.address,
token: Relay.addresses.alphaUsd,
} as never)
expect({
feePayer: ownerFirstReceipt.feePayer,
status: ownerFirstReceipt.status,
}).toMatchInlineSnapshot(`
{
"feePayer": "${feePayerAccount.address.toLowerCase()}",
"status": "success",
}
`)
const feePayerFirst = Account.fromMultisig({
address: 'infer',
owners: [owner_1.address, owner_2.address],
salt: toHex(0x109705, { size: 32 }),
threshold: 2,
})
await Actions.token.transferSync(caller, {
account: feePayerAccount,
amount: parseUnits('1', 6),
to: feePayerFirst.address,
token: Relay.addresses.alphaUsd,
})
const { receipt: feePayerFirstPending } = await Actions.token.transferSync(sponsorClient, {
account: feePayerFirst,
amount: 7n,
feePayer: feePayerAccount,
owner: owner_1,
to: recipient.address,
token: Relay.addresses.alphaUsd,
})
if (!feePayerFirstPending.multisig) throw new Error('Expected a multisig operation.')
const feePayerFirstTransaction = Transaction.deserialize(
feePayerFirstPending.multisig.transaction,
)
expect({
feePayerSigned:
'feePayerSignature' in feePayerFirstTransaction &&
!!feePayerFirstTransaction.feePayerSignature,
status: feePayerFirstPending.status,
}).toMatchInlineSnapshot(`
{
"feePayerSigned": true,
"status": "pending",
}
`)
const { receipt: feePayerFirstReceipt } = await Actions.token.transferSync(sponsorClient, {
account: feePayerFirst,
amount: 7n,
hash: feePayerFirstPending.transactionHash,
owner: owner_2,
to: recipient.address,
token: Relay.addresses.alphaUsd,
} as never)
expect({
feePayer: feePayerFirstReceipt.feePayer,
status: feePayerFirstReceipt.status,
}).toMatchInlineSnapshot(`
{
"feePayer": "${feePayerAccount.address.toLowerCase()}",
"status": "success",
}
`)
} finally {
sponsorServer.close()
}
})
test('example: initial config and immediate access key use', async () => {
const owner_1 = Relay.accounts[3]!
const owner_2 = Relay.accounts[4]!
const account = Account.fromMultisig({
address: 'infer',
owners: [owner_1, owner_2],
salt: toHex(0x109706, { size: 32 }),
threshold: 2,
})
const accessKey = Account.fromSecp256k1(generatePrivateKey(), {
access: account,
})
await Actions.token.transferSync(caller, {
account: feePayerAccount,
amount: parseUnits('1', 6),
to: account.address,
token: Relay.addresses.alphaUsd,
})
const pending = await Actions.accessKey.signAuthorization(client, {
accessKey,
account,
owner: owner_1,
})
expect({
signatureCount: pending.multisig.signatureCount,
status: pending.status,
threshold: pending.multisig.threshold,
weight: pending.multisig.weight,
}).toMatchInlineSnapshot(`
{
"signatureCount": 1,
"status": "pending",
"threshold": 2,
"weight": 1,
}
`)
const keyAuthorization = await Actions.accessKey.signAuthorization(client, {
hash: pending.hash,
owner: owner_2,
})
const { receipt } = await Actions.token.transferSync(client, {
account: accessKey,
amount: 8n,
keyAuthorization,
to: recipient.address,
token: Relay.addresses.alphaUsd,
})
const transaction = await getTransaction(client, {
hash: receipt.transactionHash,
})
expect({
from: receipt.from,
keyAuthorizationSignature: transaction.keyAuthorization?.signature.type,
status: receipt.status,
transactionSignature: transaction.signature?.type,
version:
transaction.keyAuthorization?.signature.type === 'multisig'
? transaction.keyAuthorization.signature.config.version
: undefined,
}).toMatchInlineSnapshot(`
{
"from": "${account.address.toLowerCase()}",
"keyAuthorizationSignature": "multisig",
"status": "success",
"transactionSignature": "keychain",
"version": 0n,
}
`)
})
test('example: initial config and subsequent access key use', async () => {
const owner_1 = Relay.accounts[6]!
const owner_2 = Relay.accounts[8]!
const account = Account.fromMultisig({
address: 'infer',
owners: [owner_1, owner_2],
salt: toHex(0x109707, { size: 32 }),
threshold: 2,
})
const accessKey = Account.fromSecp256k1(generatePrivateKey(), {
access: account,
})
await Actions.token.transferSync(caller, {
account: feePayerAccount,
amount: parseUnits('1', 6),
to: account.address,
token: Relay.addresses.alphaUsd,
})
const pendingAuthorization = await Actions.accessKey.signAuthorization(client, {
accessKey,
account,
owner: owner_1,
})
const keyAuthorization = await Actions.accessKey.signAuthorization(client, {
hash: pendingAuthorization.hash,
owner: owner_2,
})
const { receipt: pending } = await Actions.token.transferSync(client, {
account,
amount: 9n,
keyAuthorization,
owner: owner_1,
to: recipient.address,
token: Relay.addresses.alphaUsd,
})
const { receipt: initialReceipt } = await Actions.token.transferSync(client, {
account,
amount: 9n,
hash: pending.transactionHash,
keyAuthorization,
owner: owner_2,
to: recipient.address,
token: Relay.addresses.alphaUsd,
} as never)
const initialTransaction = await getTransaction(client, {
hash: initialReceipt.transactionHash,
})
expect({
keyAuthorizationSignature: initialTransaction.keyAuthorization?.signature.type,
status: initialReceipt.status,
transactionSignature: initialTransaction.signature?.type,
}).toMatchInlineSnapshot(`
{
"keyAuthorizationSignature": "multisig",
"status": "success",
"transactionSignature": "multisig",
}
`)
const { receipt } = await Actions.token.transferSync(client, {
account: accessKey,
amount: 10n,
to: recipient.address,
token: Relay.addresses.alphaUsd,
})
expect({
from: receipt.from,
status: receipt.status,
}).toMatchInlineSnapshot(`
{
"from": "${account.address.toLowerCase()}",
"status": "success",
}
`)
})
test('example: current configuration and configuration rotation', async () => {
const owner_1 = Relay.accounts[1]!
const owner_2 = Relay.accounts[2]!
const owner_3 = Relay.accounts[3]!
const account = Account.fromMultisig({
address: 'infer',
owners: [owner_1, owner_2],
salt: toHex(0x109708, { size: 32 }),
threshold: 2,
})
const nextConfig = {
owners: [{ owner: owner_3.address, weight: 1 }],
threshold: 1,
} as const
await Actions.token.transferSync(caller, {
account: feePayerAccount,
amount: parseUnits('1', 6),
to: account.address,
token: Relay.addresses.alphaUsd,
})
const { receipt: pending } = await Actions.multisig.updateConfigSync(client, {
account,
nextConfig,
owner: owner_1,
})
expect({
status: pending.status,
version: pending.multisig?.config.version,
}).toMatchInlineSnapshot(`
{
"status": "pending",
"version": 0n,
}
`)
const { receipt: updateReceipt, ...rotation } = await Actions.multisig.updateConfigSync(
client,
{
account,
hash: pending.transactionHash,
nextConfig,
owner: owner_2,
} as never,
)
expect({
account: rotation.account,
status: updateReceipt.status,
threshold: rotation.config.threshold,
version: rotation.config.version,
}).toMatchInlineSnapshot(`
{
"account": "${account.address}",
"status": "success",
"threshold": 1,
"version": 1n,
}
`)
const currentAccount = Account.fromMultisig(account.address)
const { receipt } = await Actions.token.transferSync(client, {
account: currentAccount,
amount: 11n,
owner: owner_3,
to: recipient.address,
token: Relay.addresses.alphaUsd,
})
const transaction = await getTransaction(client, {
hash: receipt.transactionHash,
})
expect({
account: transaction.from,
status: receipt.status,
version:
transaction.signature?.type === 'multisig'
? transaction.signature.config.version
: undefined,
}).toMatchInlineSnapshot(`
{
"account": "${account.address.toLowerCase()}",
"status": "success",
"version": 1n,
}
`)
})
test('behavior: routes pathless approvals to their operation chain', async () => {
const owner_1 = Relay.accounts[1]!
const owner_2 = Relay.accounts[2]!
const account = Account.fromMultisig({
address: 'infer',
owners: [owner_1, owner_2],
salt: toHex(0x109709, { size: 32 }),
threshold: 2,
})
await Actions.token.transferSync(caller, {
account: feePayerAccount,
amount: parseUnits('1', 6),
to: account.address,
token: Relay.addresses.alphaUsd,
})
const { receipt: pending } = await Actions.token.transferSync(client, {
account,
amount: 12n,
owner: owner_1,
to: recipient.address,
token: Relay.addresses.alphaUsd,
})
const getClient = Relay.readClient()
if (!getClient) throw new Error('Expected a relay client resolver.')
const routedServer = await Relay.createServer(
(
await Relay.relay({
getClient(chainId) {
if (chainId !== Relay.chain.id)
throw new Error('Expected the multisig operation chain.')
return getClient(chainId)
},
multisig: { store },
tokens: localnetTokens,
})
).listener,
)
try {
const routedClient = createClient({
chain: Relay.chain,
pollingInterval: 100,
transport: withRelay(Relay.http(), Relay.http(routedServer.url)),
})
const { receipt } = await Actions.token.transferSync(routedClient, {
account,
amount: 12n,
hash: pending.transactionHash,
owner: owner_2,
to: recipient.address,
token: Relay.addresses.alphaUsd,
} as never)
expect(receipt.status).toMatchInlineSnapshot(`"success"`)
} finally {
await routedServer.closeAsync()
}
})
},
)
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('rejects redirects from external fee-payer relays', async () => {
let targetRequests = 0
const target = await Relay.createServer((_request, response) => {
targetRequests++
response.end()
})
const redirect = await Relay.createServer((_request, response) => {
response.writeHead(302, { location: target.url })
response.end()
})
try {
await expect(
fillTransaction(client, {
account: userAccount.address,
calls: [transferCall()],
feePayer: redirect.url as never,
}),
).rejects.toThrow()
expect(targetRequests).toBe(0)
} finally {
redirect.close()
target.close()
}
})
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()
})
})