accounts 0.19.0 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,398 @@
1
+ import {
2
+ encodeFunctionData,
3
+ http as viem_http,
4
+ isAddressEqual,
5
+ parseUnits,
6
+ toFunctionSelector,
7
+ } from 'viem'
8
+ import { generatePrivateKey } from 'viem/accounts'
9
+ import { fillTransaction, sendTransactionSync } from 'viem/actions'
10
+ import { Abis, Account, Actions, Addresses } from 'viem/tempo'
11
+ import { afterEach, expect, test } from 'vp/test'
12
+
13
+ import {
14
+ accounts,
15
+ addresses,
16
+ chain as localnetChain,
17
+ getClient,
18
+ rpcUrl,
19
+ } from '../../../../test/config.js'
20
+ import { createServer, type Server } from '../../../../test/utils.js'
21
+ import * as Kv from '../../Kv.js'
22
+ import * as FeeLiquidity from './feeLiquidity.js'
23
+ import { relay } from './relay.js'
24
+
25
+ const servers: Server[] = []
26
+ const chain = { ...localnetChain, hardfork: 't5' as const }
27
+ const balanceSelector = toFunctionSelector('balanceOf(address)').slice(2)
28
+ const poolSelector = toFunctionSelector('getPool(address,address)').slice(2)
29
+
30
+ afterEach(async () => {
31
+ await Promise.all(servers.splice(0).map((server) => server.closeAsync()))
32
+ })
33
+
34
+ async function fixture() {
35
+ const rpc = getClient({ account: accounts[0]! })
36
+ const sender = Account.fromSecp256k1(generatePrivateKey())
37
+ const { token } = await Actions.token.createSync(rpc, {
38
+ name: 'Localnet MACH',
39
+ symbol: 'MACH',
40
+ currency: 'USD',
41
+ quoteToken: addresses.alphaUsd,
42
+ })
43
+ await sendTransactionSync(rpc, {
44
+ feeToken: Addresses.pathUsd,
45
+ calls: [
46
+ Actions.token.grantRoles.call({ token, role: 'issuer', to: rpc.account.address }),
47
+ Actions.token.mint.call({ token, amount: parseUnits('500', 6), to: sender.address }),
48
+ Actions.token.mint.call({
49
+ token: addresses.alphaUsd,
50
+ amount: parseUnits('100', 6),
51
+ to: sender.address,
52
+ }),
53
+ ],
54
+ })
55
+ await sendTransactionSync(rpc, {
56
+ account: sender,
57
+ feeToken: addresses.alphaUsd,
58
+ calls: [Actions.fee.setUserToken.call({ token })],
59
+ throwOnReceiptRevert: true,
60
+ })
61
+ const calls = [
62
+ Actions.token.transfer.call({
63
+ token: addresses.alphaUsd,
64
+ to: accounts[7]!.address,
65
+ amount: 1n,
66
+ }),
67
+ ]
68
+ const tokens = [token, addresses.alphaUsd].map((address) => ({
69
+ address,
70
+ decimals: 6,
71
+ name: 'Localnet USD',
72
+ symbol: 'USD',
73
+ }))
74
+ return { rpc, sender, token, calls, tokens }
75
+ }
76
+
77
+ function observe(
78
+ options: {
79
+ beforeBalance?: (() => Promise<void>) | undefined
80
+ beforeProducer?: (() => Promise<void>) | undefined
81
+ onProducer?: (() => void) | undefined
82
+ onPool?: (() => void) | undefined
83
+ } = {},
84
+ ) {
85
+ const events: string[] = []
86
+ const pools: string[] = []
87
+ let balances = 0
88
+ const transport = viem_http(rpcUrl, {
89
+ retryCount: 0,
90
+ timeout: 5_000,
91
+ fetchFn: async (url, init) => {
92
+ const body = JSON.parse(init?.body as string) as
93
+ | { method: string; params?: { data?: string }[] }
94
+ | { method: string; params?: { data?: string }[] }[]
95
+ const requests = Array.isArray(body) ? body : [body]
96
+ let balance = false
97
+ let pool = false
98
+ let producer = false
99
+ for (const request of requests) {
100
+ if (request.method === 'eth_getBlockByNumber') producer = true
101
+ if (request.method === 'eth_fillTransaction') events.push('fill')
102
+ if (request.method !== 'eth_call') continue
103
+ const data = request.params?.[0]?.data?.toLowerCase() ?? ''
104
+ const matches = data.match(new RegExp(balanceSelector, 'g')) ?? []
105
+ if (matches.length > 0) {
106
+ balances += matches.length
107
+ balance = true
108
+ events.push('balance')
109
+ }
110
+ for (const match of data.matchAll(new RegExp(`${poolSelector}[0-9a-f]{128}`, 'g'))) {
111
+ pool = true
112
+ pools.push(match[0])
113
+ events.push('pool')
114
+ }
115
+ }
116
+ if (producer) {
117
+ await options.beforeProducer?.()
118
+ events.push('producer')
119
+ options.onProducer?.()
120
+ }
121
+ if (pool) options.onPool?.()
122
+ if (balance) await options.beforeBalance?.()
123
+ const response = await fetch(url, init)
124
+ if (balance) events.push('balance-complete')
125
+ return response
126
+ },
127
+ })
128
+ return {
129
+ transport,
130
+ events,
131
+ pools,
132
+ get balances() {
133
+ return balances
134
+ },
135
+ }
136
+ }
137
+
138
+ async function serve(options: relay.Options) {
139
+ const server = await createServer(relay({ autoSwap: false, ...options }).listener)
140
+ servers.push(server)
141
+ return getClient({ transport: viem_http(server.url, { retryCount: 0 }) })
142
+ }
143
+
144
+ test('fee liquidity: producer discovery overlaps real pending balance RPCs', async () => {
145
+ const state = await fixture()
146
+ const balance = Promise.withResolvers<void>()
147
+ const producer = Promise.withResolvers<void>()
148
+ const observed = observe({
149
+ beforeBalance: () => {
150
+ balance.resolve()
151
+ return producer.promise
152
+ },
153
+ beforeProducer: () => balance.promise,
154
+ onProducer: () => producer.resolve(),
155
+ })
156
+ const client = await serve({
157
+ chains: [chain],
158
+ features: 'all',
159
+ resolveTokens: () => state.tokens,
160
+ transports: { [chain.id]: observed.transport },
161
+ })
162
+ const { transaction } = await fillTransaction(client, {
163
+ account: state.sender.address,
164
+ calls: state.calls,
165
+ })
166
+ const receipt = await sendTransactionSync(getClient(), {
167
+ type: 'tempo',
168
+ calls: transaction.calls!,
169
+ feeToken: transaction.feeToken!,
170
+ gas: transaction.gas,
171
+ maxFeePerGas: transaction.maxFeePerGas,
172
+ maxPriorityFeePerGas: transaction.maxPriorityFeePerGas,
173
+ nonce: transaction.nonce,
174
+ account: state.sender,
175
+ })
176
+ expect({
177
+ overlaps: observed.events.indexOf('producer') < observed.events.indexOf('balance-complete'),
178
+ balanceStarted: observed.events.indexOf('balance') < observed.events.indexOf('producer'),
179
+ skippedMach: isAddressEqual(transaction.feeToken!, addresses.alphaUsd),
180
+ balances: observed.balances,
181
+ status: receipt.status,
182
+ }).toMatchInlineSnapshot(`
183
+ {
184
+ "balanceStarted": true,
185
+ "balances": 2,
186
+ "overlaps": true,
187
+ "skippedMach": true,
188
+ "status": "success",
189
+ }
190
+ `)
191
+ })
192
+
193
+ test('fee liquidity: pool reads overlap real pending balances and are reused across retries', async () => {
194
+ const state = await fixture()
195
+ const pool = Promise.withResolvers<void>()
196
+ const observed = observe({ beforeBalance: () => pool.promise, onPool: () => pool.resolve() })
197
+ const client = await serve({
198
+ chains: [chain],
199
+ features: 'all',
200
+ resolveTokens: () => state.tokens,
201
+ transports: { [chain.id]: observed.transport },
202
+ })
203
+ const { transaction } = await fillTransaction(client, {
204
+ account: state.sender.address,
205
+ calls: state.calls,
206
+ })
207
+ const direct = encodeFunctionData({
208
+ abi: Abis.feeAmm,
209
+ functionName: 'getPool',
210
+ args: [state.token, Addresses.pathUsd],
211
+ })
212
+ .slice(2)
213
+ .toLowerCase()
214
+ expect({
215
+ overlaps: observed.events.indexOf('pool') < observed.events.indexOf('balance-complete'),
216
+ balanceStarted: observed.events.indexOf('balance') < observed.events.indexOf('pool'),
217
+ directReads: observed.pools.filter((call) => call === direct).length,
218
+ balances: observed.balances,
219
+ skippedMach: isAddressEqual(transaction.feeToken!, addresses.alphaUsd),
220
+ }).toMatchInlineSnapshot(`
221
+ {
222
+ "balanceStarted": true,
223
+ "balances": 2,
224
+ "directReads": 1,
225
+ "overlaps": true,
226
+ "skippedMach": true,
227
+ }
228
+ `)
229
+ })
230
+
231
+ test('fee liquidity: reads an unlisted on-chain preference without rediscovering balances', async () => {
232
+ const state = await fixture()
233
+ const observed = observe()
234
+ const client = await serve({
235
+ chains: [chain],
236
+ features: 'all',
237
+ resolveTokens: () =>
238
+ state.tokens.filter((token) => isAddressEqual(token.address, addresses.alphaUsd)),
239
+ transports: { [chain.id]: observed.transport },
240
+ })
241
+ const { transaction } = await fillTransaction(client, {
242
+ account: state.sender.address,
243
+ calls: state.calls,
244
+ })
245
+ expect({
246
+ balances: observed.balances,
247
+ skippedMach: isAddressEqual(transaction.feeToken!, addresses.alphaUsd),
248
+ }).toMatchInlineSnapshot(`
249
+ {
250
+ "balances": 2,
251
+ "skippedMach": true,
252
+ }
253
+ `)
254
+ })
255
+
256
+ test('fee liquidity: explicit fee tokens do not trigger user balance or reserve discovery', async () => {
257
+ const state = await fixture()
258
+ const observed = observe()
259
+ const client = await serve({
260
+ chains: [chain],
261
+ features: 'all',
262
+ resolveTokens: () => state.tokens,
263
+ transports: { [chain.id]: observed.transport },
264
+ })
265
+ const { transaction } = await fillTransaction(client, {
266
+ account: state.sender.address,
267
+ calls: state.calls,
268
+ feeToken: addresses.alphaUsd,
269
+ capabilities: { balanceDiffs: false },
270
+ })
271
+ expect({
272
+ balances: observed.balances,
273
+ pools: observed.pools.length,
274
+ preserved: isAddressEqual(transaction.feeToken!, addresses.alphaUsd),
275
+ }).toMatchInlineSnapshot(`
276
+ {
277
+ "balances": 0,
278
+ "pools": 0,
279
+ "preserved": true,
280
+ }
281
+ `)
282
+ })
283
+
284
+ test('fee liquidity: sponsored fills do not probe user balances or pools', async () => {
285
+ const state = await fixture()
286
+ const observed = observe()
287
+ const client = await serve({
288
+ chains: [chain],
289
+ features: 'all',
290
+ feePayer: { account: accounts[0]!, feeToken: Addresses.pathUsd },
291
+ resolveTokens: () => state.tokens,
292
+ transports: { [chain.id]: observed.transport },
293
+ })
294
+ const { transaction } = await fillTransaction(client, {
295
+ account: state.sender.address,
296
+ calls: state.calls,
297
+ capabilities: { balanceDiffs: false },
298
+ })
299
+ expect({
300
+ balances: observed.balances,
301
+ pools: observed.pools.length,
302
+ sponsorToken: isAddressEqual(transaction.feeToken!, Addresses.pathUsd),
303
+ }).toMatchInlineSnapshot(`
304
+ {
305
+ "balances": 0,
306
+ "pools": 0,
307
+ "sponsorToken": true,
308
+ }
309
+ `)
310
+ })
311
+
312
+ test('fee liquidity: reserve snapshots stay request-scoped after on-chain pool funding', async () => {
313
+ const state = await fixture()
314
+ const observed = observe()
315
+ const client = getClient({ transport: observed.transport })
316
+ const check = await FeeLiquidity.prepare(client, { token: state.token })
317
+ expect(await check(2057n)).toMatchInlineSnapshot(`false`)
318
+ await Actions.amm.mintSync(state.rpc, {
319
+ feeToken: Addresses.pathUsd,
320
+ userTokenAddress: state.token,
321
+ validatorTokenAddress: Addresses.pathUsd,
322
+ validatorTokenAmount: 2050n,
323
+ to: state.rpc.account.address,
324
+ })
325
+ const reads = observed.pools.length
326
+ expect({ stale: await check(2057n), rereads: observed.pools.length - reads })
327
+ .toMatchInlineSnapshot(`
328
+ {
329
+ "rereads": 0,
330
+ "stale": false,
331
+ }
332
+ `)
333
+ const fresh = await FeeLiquidity.prepare(client, { token: state.token })
334
+ expect({ covered: await fresh(2057n), insufficient: await fresh(2058n) }).toMatchInlineSnapshot(`
335
+ {
336
+ "covered": true,
337
+ "insufficient": false,
338
+ }
339
+ `)
340
+ })
341
+
342
+ test('fee liquidity: a real two-hop pool settles the selected fee token at broadcast', async () => {
343
+ const state = await fixture()
344
+ await Actions.amm.mintSync(state.rpc, {
345
+ feeToken: Addresses.pathUsd,
346
+ userTokenAddress: state.token,
347
+ validatorTokenAddress: addresses.alphaUsd,
348
+ validatorTokenAmount: parseUnits('1', 6),
349
+ to: state.rpc.account.address,
350
+ })
351
+ const client = await serve({
352
+ chains: [chain],
353
+ features: 'all',
354
+ resolveTokens: () => state.tokens,
355
+ transports: { [chain.id]: viem_http(rpcUrl) },
356
+ })
357
+ const { transaction } = await fillTransaction(client, {
358
+ account: state.sender.address,
359
+ calls: state.calls,
360
+ })
361
+ const receipt = await sendTransactionSync(getClient(), {
362
+ account: state.sender,
363
+ type: 'tempo',
364
+ calls: transaction.calls!,
365
+ feeToken: transaction.feeToken!,
366
+ gas: transaction.gas,
367
+ maxFeePerGas: transaction.maxFeePerGas,
368
+ maxPriorityFeePerGas: transaction.maxPriorityFeePerGas,
369
+ nonce: transaction.nonce,
370
+ })
371
+ expect({
372
+ selectedMach: isAddressEqual(transaction.feeToken!, state.token),
373
+ status: receipt.status,
374
+ }).toMatchInlineSnapshot(`
375
+ {
376
+ "selectedMach": true,
377
+ "status": "success",
378
+ }
379
+ `)
380
+ })
381
+
382
+ test('fee liquidity: validator tokens need no pool reads', async () => {
383
+ const observed = observe()
384
+ const client = getClient({ transport: observed.transport })
385
+ const kv = Kv.memory()
386
+ const checks = await Promise.all([
387
+ FeeLiquidity.prepare(client, { token: Addresses.pathUsd, kv }),
388
+ FeeLiquidity.prepare(client, { token: addresses.alphaUsd, kv }),
389
+ ])
390
+ expect({ validator: await checks[0]!(206n), alpha: await checks[1]!(206n) })
391
+ .toMatchInlineSnapshot(`
392
+ {
393
+ "alpha": true,
394
+ "validator": true,
395
+ }
396
+ `)
397
+ expect(observed.events.filter((event) => event === 'producer').length).toMatchInlineSnapshot(`10`)
398
+ })
@@ -0,0 +1,117 @@
1
+ import { type Address, type Client, isAddressEqual, zeroAddress } from 'viem'
2
+ import { getBlock, readContract } from 'viem/actions'
3
+ import { Abis, Actions, Addresses, Hardfork } from 'viem/tempo'
4
+
5
+ import type * as Kv from '../../Kv.js'
6
+ import { cached } from '../kv.js'
7
+
8
+ /** Number of recent block producers the node's pool admission considers. */
9
+ const producerWindow = 10
10
+
11
+ /**
12
+ * Checks whether the fee AMM can settle the fee for at least one recent block
13
+ * producer's preferred token. Mirrors the node's pool admission check, so a
14
+ * token is only rejected here when the node would reject it too.
15
+ */
16
+ export async function has(client: Client, options: has.Options): Promise<boolean> {
17
+ const check = await prepare(client, options)
18
+ return check(options.amount)
19
+ }
20
+
21
+ /** Reads fee reserves ahead of filling and returns a reusable maximum-fee check. */
22
+ export async function prepare(client: Client, options: prepare.Options) {
23
+ const { kv, token } = options
24
+ const validatorTokens = await getValidatorTokens(client, { kv })
25
+ if (validatorTokens.some((validatorToken) => isAddressEqual(validatorToken, token)))
26
+ return async (_amount: bigint) => true
27
+
28
+ const hardfork = (client.chain as { hardfork?: string } | undefined)?.hardfork
29
+ const twoHop = !hardfork || !Hardfork.lt(hardfork, 't5')
30
+
31
+ const [direct, quote] = await Promise.all([
32
+ Promise.all(
33
+ validatorTokens.map((validatorToken) =>
34
+ Actions.amm.getPool(client, { userToken: token, validatorToken }),
35
+ ),
36
+ ),
37
+ twoHop
38
+ ? readContract(client, { address: token, abi: Abis.tip20, functionName: 'quoteToken' })
39
+ : undefined,
40
+ ])
41
+ const targets = quote
42
+ ? validatorTokens.filter((validatorToken) => !isAddressEqual(validatorToken, quote))
43
+ : []
44
+ const route =
45
+ quote && quote !== zeroAddress && targets.length > 0
46
+ ? Promise.all([
47
+ Actions.amm.getPool(client, { userToken: token, validatorToken: quote }),
48
+ ...targets.map((validatorToken) =>
49
+ Actions.amm.getPool(client, { userToken: quote, validatorToken }),
50
+ ),
51
+ ])
52
+ : undefined
53
+ void route?.catch(() => {})
54
+ return async (amount: bigint) => {
55
+ const output = (amount * 9970n) / 10000n
56
+ if (direct.some((pool) => pool.reserveValidatorToken >= output)) return true
57
+ if (!route) return false
58
+ const [first, ...second] = await route
59
+ if (first!.reserveValidatorToken < output) return false
60
+ return second.some((pool) => pool.reserveValidatorToken >= (output * 9970n) / 10000n)
61
+ }
62
+ }
63
+
64
+ /** Parameters for prefetching a token's fee reserves. */
65
+ export declare namespace prepare {
66
+ /** Fee token and producer-preference cache. */
67
+ type Options = Pick<has.Options, 'token' | 'kv'>
68
+ }
69
+
70
+ /** Parameters for the fee-liquidity check. */
71
+ export declare namespace has {
72
+ /** Fee token and maximum fee, in TIP-20 base units. */
73
+ type Options = {
74
+ /** User's fee token. */
75
+ token: Address
76
+ /** Maximum transaction fee, rounded up to microdollars. */
77
+ amount: bigint
78
+ /** Caches the recent producers' fee tokens across fills. */
79
+ kv?: Kv.Kv | undefined
80
+ }
81
+ }
82
+
83
+ /** Resolves the unique fee tokens preferred by the producers of the latest blocks. */
84
+ async function getValidatorTokens(
85
+ client: Client,
86
+ options: { kv?: Kv.Kv | undefined },
87
+ ): Promise<readonly Address[]> {
88
+ async function load() {
89
+ const latest = await getBlock(client)
90
+ const number = latest.number ?? 0n
91
+ const previous = await Promise.all(
92
+ Array.from({ length: producerWindow - 1 }, (_, index) => number - BigInt(index + 1))
93
+ .filter((blockNumber) => blockNumber >= 0n)
94
+ .map((blockNumber) => getBlock(client, { blockNumber })),
95
+ )
96
+ const validators = unique([latest, ...previous].map((block) => block.miner))
97
+ const tokens = await Promise.all(
98
+ validators.map(async (validator) => {
99
+ const preference = await Actions.fee.getValidatorToken(client, { validator })
100
+ return preference?.address ?? Addresses.pathUsd
101
+ }),
102
+ )
103
+ return unique(tokens)
104
+ }
105
+
106
+ // Producer preferences rarely change, and a stale entry only delays picking
107
+ // up a new producer's token until the cache expires.
108
+ const { kv } = options
109
+ if (!kv) return load()
110
+ return cached(kv, `fee.validatorTokens:${client.chain?.id ?? 0}`, load, { ttl: 60 })
111
+ }
112
+
113
+ function unique(addresses: readonly Address[]): Address[] {
114
+ const seen = new Map<string, Address>()
115
+ for (const address of addresses) seen.set(address.toLowerCase(), address)
116
+ return [...seen.values()]
117
+ }
@@ -938,7 +938,11 @@ describe('behavior: capabilities', () => {
938
938
  amount: parseUnits('1000', 6),
939
939
  to: sender.address,
940
940
  })
941
- await Actions.fee.setUserToken(getClient({ account: sender }), { token: addresses.alphaUsd })
941
+ await sendTransactionSync(getClient({ account: sender }), {
942
+ feeToken: addresses.alphaUsd,
943
+ calls: [Actions.fee.setUserToken.call({ token: addresses.alphaUsd })],
944
+ throwOnReceiptRevert: true,
945
+ })
942
946
 
943
947
  const buyAmount = parseUnits('10', 6)
944
948
  const result = await fillTransaction(client, {
@@ -965,6 +969,9 @@ describe('behavior: capabilities', () => {
965
969
  const meta = result.capabilities
966
970
 
967
971
  const diffs = findDiffs(meta?.balanceDiffs, sender.address)!
972
+ expect(result.transaction.feeToken).toMatchInlineSnapshot(
973
+ `"0x20c0000000000000000000000000000000000001"`,
974
+ )
968
975
  expect(diffs).toHaveLength(1)
969
976
 
970
977
  const quoteDiff = diffs[0]!
@@ -1698,7 +1705,7 @@ describe('behavior: fee token resolution', () => {
1698
1705
  expect(transaction.feeToken).toBe(preferredToken)
1699
1706
  })
1700
1707
 
1701
- test('behavior: resolves to highest-balance token from token list', async () => {
1708
+ test('behavior: skips highest-balance token until its fee pool has liquidity', async () => {
1702
1709
  // Create a fresh TIP20 token "betaUsd" so we can test highest-balance
1703
1710
  // selection without depending on a pre-deployed second token.
1704
1711
  const freshAccount = accounts[5]!
@@ -1751,10 +1758,23 @@ describe('behavior: fee token resolution', () => {
1751
1758
  account: freshAccount.address,
1752
1759
  calls: [transferCall()],
1753
1760
  })
1754
- customServer.close()
1761
+ // betaUsd has the larger balance but no fee pool yet.
1762
+ expect(transaction.feeToken?.toLowerCase()).toBe(addresses.alphaUsd.toLowerCase())
1755
1763
 
1756
- // betaUsd has higher balance (500 > 100).
1757
- expect(transaction.feeToken?.toLowerCase()).toBe(betaUsd.toLowerCase())
1764
+ await Actions.amm.mintSync(rpc, {
1765
+ account: accounts[0]!,
1766
+ feeToken: Addresses.pathUsd,
1767
+ userTokenAddress: betaUsd,
1768
+ validatorTokenAddress: Addresses.pathUsd,
1769
+ validatorTokenAmount: parseUnits('1', 6),
1770
+ to: accounts[0]!.address,
1771
+ })
1772
+ const funded = await fillTransaction(customClient, {
1773
+ account: freshAccount.address,
1774
+ calls: [transferCall()],
1775
+ })
1776
+ customServer.close()
1777
+ expect(funded.transaction.feeToken?.toLowerCase()).toBe(betaUsd.toLowerCase())
1758
1778
  })
1759
1779
 
1760
1780
  test('behavior: falls back to pathUSD when no preference or balances', async () => {
@@ -1970,13 +1990,16 @@ describe('behavior: mainnet autoSwap with USDC.e → PathUSD', () => {
1970
1990
  const mainnetUsdce = '0x20c000000000000000000000b9537d11c60e8b50' as const
1971
1991
  const mainnetPathUsd = '0x20c0000000000000000000000000000000000000' as const
1972
1992
  const mainnetSender = '0xb472f3ca15f34Db22d43FA503043F1e6541AC085' as const
1993
+ const mainnetRpcUrl = 'https://rpc.presto.tempo.xyz'
1994
+ // Keep the swap small: this runs against live mainnet DEX liquidity.
1995
+ const amount = parseUnits('0.001', 6)
1973
1996
 
1974
1997
  beforeAll(async () => {
1975
1998
  server = await createServer(
1976
1999
  relay({
1977
2000
  chains: [tempo],
1978
2001
  features: 'all',
1979
- transports: { [tempo.id]: http('https://rpc.presto.tempo.xyz') },
2002
+ transports: { [tempo.id]: http(mainnetRpcUrl) },
1980
2003
  }).listener,
1981
2004
  )
1982
2005
  mainnetClient = getClient({ chain: tempo, transport: http(server.url) })
@@ -1986,14 +2009,27 @@ describe('behavior: mainnet autoSwap with USDC.e → PathUSD', () => {
1986
2009
  server.close()
1987
2010
  })
1988
2011
 
1989
- test('behavior: auto-swaps USDC.e → PathUSD when sender has USDC.e but no PathUSD', async () => {
2012
+ test('behavior: auto-swaps USDC.e → PathUSD when sender has USDC.e but no PathUSD', async (context) => {
2013
+ // Skip rather than fail when live mainnet DEX liquidity can't cover the swap.
2014
+ const quotable = await Actions.dex
2015
+ .getBuyQuote(getClient({ chain: tempo, transport: http(mainnetRpcUrl) }), {
2016
+ amountOut: amount,
2017
+ tokenIn: mainnetUsdce,
2018
+ tokenOut: mainnetPathUsd,
2019
+ })
2020
+ .then(
2021
+ () => true,
2022
+ () => false,
2023
+ )
2024
+ if (!quotable) context.skip('mainnet DEX has insufficient USDC.e → PathUSD liquidity')
2025
+
1990
2026
  const result = await fillTransaction(mainnetClient, {
1991
2027
  account: mainnetSender,
1992
2028
  calls: [
1993
2029
  Actions.token.transfer.call({
1994
2030
  token: mainnetPathUsd,
1995
2031
  to: recipient.address,
1996
- amount: parseUnits('0.5', 6),
2032
+ amount,
1997
2033
  }),
1998
2034
  ],
1999
2035
  })