Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fresh-cycles-trade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'mppx': patch
---

Fixed Tempo auto-swap call building with `viem@>=2.54` and hardened account balance metadata formatting.
38 changes: 36 additions & 2 deletions src/cli/utils.test.ts
Original file line number Diff line number Diff line change
@@ -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(() => {
Expand Down Expand Up @@ -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,
})
})
})
3 changes: 2 additions & 1 deletion src/cli/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}

Expand Down
35 changes: 26 additions & 9 deletions src/tempo/internal/auto-swap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> }[] = []
const builderCalls: { client: unknown; name: string; parameters: Record<string, unknown> }[] =
[]

function getBalanceCall(client: unknown, parameters: Record<string, unknown>) {
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<string, unknown>) {
tokenCalls.push({ client, name: 'approve', parameters })
builderCalls.push({ client, name: 'approve', parameters })
return { kind: 'approve' }
}

function buyCall(client: unknown, parameters: Record<string, unknown>) {
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,
Expand All @@ -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: {
Expand All @@ -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')
Expand Down
17 changes: 15 additions & 2 deletions src/tempo/internal/auto-swap.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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<typeof viem_dex.buy.call> {
const call = Actions.dex.buy.call as unknown as {
length: number
(parameters: viem_dex.buy.Args): ReturnType<typeof viem_dex.buy.call>
(client: Client, parameters: viem_dex.buy.Args): ReturnType<typeof viem_dex.buy.call>
}
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.
Expand Down Expand Up @@ -100,7 +113,7 @@ export async function findCalls(
spender: Addresses.stablecoinDex,
token: tokenIn,
}),
Actions.dex.buy.call({
getBuyCall(client, {
tokenIn,
tokenOut,
amountOut,
Expand Down
Loading