From 58e5a0ad25aa491149cbc6555488b68aca78c72e Mon Sep 17 00:00:00 2001 From: Brendan Ryan <1572504+brendanjryan@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:51:57 -0700 Subject: [PATCH] fix: support viem auto-swap calls --- .changeset/fresh-cycles-trade.md | 5 ++++ src/cli/utils.test.ts | 38 ++++++++++++++++++++++++++-- src/cli/utils.ts | 3 ++- src/tempo/internal/auto-swap.test.ts | 35 ++++++++++++++++++------- src/tempo/internal/auto-swap.ts | 17 +++++++++++-- 5 files changed, 84 insertions(+), 14 deletions(-) create mode 100644 .changeset/fresh-cycles-trade.md diff --git a/.changeset/fresh-cycles-trade.md b/.changeset/fresh-cycles-trade.md new file mode 100644 index 00000000..344251f4 --- /dev/null +++ b/.changeset/fresh-cycles-trade.md @@ -0,0 +1,5 @@ +--- +'mppx': patch +--- + +Fixed Tempo auto-swap call building with `viem@>=2.54` and hardened account balance metadata formatting. diff --git a/src/cli/utils.test.ts b/src/cli/utils.test.ts index b942c292..b0edbcb4 100644 --- a/src/cli/utils.test.ts +++ b/src/cli/utils.test.ts @@ -1,7 +1,13 @@ import { tempo as tempoMainnet, tempoModerato } from 'viem/tempo/chains' -import { afterEach, describe, expect, test } from 'vp/test' +import { afterEach, describe, expect, test, vi } from 'vp/test' -import { networkRpcUrls, resolveChain, resolveFundingNetwork, resolveRpcUrl } from './utils.js' +import { + fetchTokenInfo, + networkRpcUrls, + resolveChain, + resolveFundingNetwork, + resolveRpcUrl, +} from './utils.js' describe('resolveRpcUrl', () => { afterEach(() => { @@ -97,3 +103,31 @@ describe('resolveChain', () => { expect(chain.id).not.toBe(tempoModerato.id) }) }) + +describe('fetchTokenInfo', () => { + afterEach(() => { + vi.doUnmock('viem/tempo') + }) + + test('uses 6 decimals when token metadata omits a numeric decimals value', async () => { + const token = '0x1111111111111111111111111111111111111111' + const account = '0x2222222222222222222222222222222222222222' + vi.doMock('viem/tempo', () => ({ + Actions: { + token: { + getBalance: vi.fn(async () => ({ amount: 123n })), + getMetadata: vi.fn(async () => ({ decimals: undefined, symbol: 'TEST' })), + }, + }, + })) + + const info = await fetchTokenInfo({} as never, token, account) + + expect(info).toEqual({ + balance: 123n, + decimals: 6, + symbol: 'TEST', + token, + }) + }) +}) diff --git a/src/cli/utils.ts b/src/cli/utils.ts index 409a6f89..b041f458 100644 --- a/src/cli/utils.ts +++ b/src/cli/utils.ts @@ -318,7 +318,8 @@ export async function fetchTokenInfo( [usdc]: 'USDC', } const symbol = knownSymbols[token] ?? metadata.symbol - const decimals = 'decimals' in metadata ? metadata.decimals : 6 + const decimals = + 'decimals' in metadata && typeof metadata.decimals === 'number' ? metadata.decimals : 6 return { balance, symbol, decimals, token } } diff --git a/src/tempo/internal/auto-swap.test.ts b/src/tempo/internal/auto-swap.test.ts index 22da8901..6daf627b 100644 --- a/src/tempo/internal/auto-swap.test.ts +++ b/src/tempo/internal/auto-swap.test.ts @@ -109,18 +109,24 @@ describe('findCalls', () => { const tokenOut = '0x2222222222222222222222222222222222222222' as Address const tokenIn = '0x3333333333333333333333333333333333333333' as Address const client = { chain: { id: 42431 } } - const tokenCalls: { client: unknown; name: string; parameters: Record }[] = [] + const builderCalls: { client: unknown; name: string; parameters: Record }[] = + [] function getBalanceCall(client: unknown, parameters: Record) { - tokenCalls.push({ client, name: 'getBalance', parameters }) + builderCalls.push({ client, name: 'getBalance', parameters }) return { kind: parameters.token === tokenOut ? 'targetBalance' : 'candidateBalance' } } function approveCall(client: unknown, parameters: Record) { - tokenCalls.push({ client, name: 'approve', parameters }) + builderCalls.push({ client, name: 'approve', parameters }) return { kind: 'approve' } } + function buyCall(client: unknown, parameters: Record) { + builderCalls.push({ client, name: 'buy', parameters }) + return { kind: 'buy' } + } + vi.doMock('viem/actions', () => ({ readContract: vi.fn(async (_client, call: { kind: string }) => call.kind === 'targetBalance' ? 0n : 2_000_000n, @@ -129,7 +135,7 @@ describe('findCalls', () => { vi.doMock('viem/tempo', () => ({ Actions: { dex: { - buy: { call: vi.fn((parameters) => ({ kind: 'buy', parameters })) }, + buy: { call: buyCall }, getBuyQuote: vi.fn(async () => 1_000_000n), }, token: { @@ -152,15 +158,26 @@ describe('findCalls', () => { }) expect(calls).toHaveLength(2) - expect(tokenCalls.map((call) => call.client)).toEqual([client, client, client]) - expect(tokenCalls.map((call) => call.name)).toEqual(['getBalance', 'getBalance', 'approve']) - expect(tokenCalls[0]!.parameters).toMatchObject({ account, token: tokenOut }) - expect(tokenCalls[1]!.parameters).toMatchObject({ account, token: tokenIn }) - expect(tokenCalls[2]!.parameters).toMatchObject({ + expect(builderCalls.map((call) => call.client)).toEqual([client, client, client, client]) + expect(builderCalls.map((call) => call.name)).toEqual([ + 'getBalance', + 'getBalance', + 'approve', + 'buy', + ]) + expect(builderCalls[0]!.parameters).toMatchObject({ account, token: tokenOut }) + expect(builderCalls[1]!.parameters).toMatchObject({ account, token: tokenIn }) + expect(builderCalls[2]!.parameters).toMatchObject({ amount: 1_010_000n, spender: '0x4444444444444444444444444444444444444444', token: tokenIn, }) + expect(builderCalls[3]!.parameters).toMatchObject({ + amountOut: 1_000_000n, + maxAmountIn: 1_010_000n, + tokenIn, + tokenOut, + }) } finally { vi.doUnmock('viem/actions') vi.doUnmock('viem/tempo') diff --git a/src/tempo/internal/auto-swap.ts b/src/tempo/internal/auto-swap.ts index 42841c31..f8b0c4a9 100644 --- a/src/tempo/internal/auto-swap.ts +++ b/src/tempo/internal/auto-swap.ts @@ -1,7 +1,7 @@ import type { Address, Client } from 'viem' import { readContract } from 'viem/actions' import { Actions, Addresses } from 'viem/tempo' -import type { token as viem_token } from 'viem/tempo/actions' +import type { dex as viem_dex, token as viem_token } from 'viem/tempo/actions' import * as TempoAddress from './address.js' import * as defaults from './defaults.js' @@ -47,6 +47,19 @@ function getApproveCall( return call.length >= 2 ? call(client, parameters) : call(parameters) } +// TODO: Remove once the minimum viem version is >=2.54.0, which uses client-first Tempo call builders. +function getBuyCall( + client: Client, + parameters: viem_dex.buy.Args, +): ReturnType { + const call = Actions.dex.buy.call as unknown as { + length: number + (parameters: viem_dex.buy.Args): ReturnType + (client: Client, parameters: viem_dex.buy.Args): ReturnType + } + return call.length >= 2 ? call(client, parameters) : call(parameters) +} + /** * Finds the optimal swap calls to acquire `amountOut` of `tokenOut`, * returning an approve + buy call sequence if a viable route is found. @@ -100,7 +113,7 @@ export async function findCalls( spender: Addresses.stablecoinDex, token: tokenIn, }), - Actions.dex.buy.call({ + getBuyCall(client, { tokenIn, tokenOut, amountOut,