accounts 0.8.6 → 0.8.8

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.
Files changed (33) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/core/Schema.d.ts +2 -0
  3. package/dist/core/Schema.d.ts.map +1 -1
  4. package/dist/core/Storage.js +1 -1
  5. package/dist/core/Storage.js.map +1 -1
  6. package/dist/core/zod/rpc.d.ts +2 -0
  7. package/dist/core/zod/rpc.d.ts.map +1 -1
  8. package/dist/core/zod/rpc.js +2 -0
  9. package/dist/core/zod/rpc.js.map +1 -1
  10. package/dist/server/internal/handlers/exchange.d.ts.map +1 -1
  11. package/dist/server/internal/handlers/exchange.js +5 -19
  12. package/dist/server/internal/handlers/exchange.js.map +1 -1
  13. package/dist/server/internal/handlers/relay.d.ts +20 -12
  14. package/dist/server/internal/handlers/relay.d.ts.map +1 -1
  15. package/dist/server/internal/handlers/relay.js +152 -84
  16. package/dist/server/internal/handlers/relay.js.map +1 -1
  17. package/dist/server/internal/kv.d.ts +7 -4
  18. package/dist/server/internal/kv.d.ts.map +1 -1
  19. package/dist/server/internal/kv.js +70 -11
  20. package/dist/server/internal/kv.js.map +1 -1
  21. package/dist/server/internal/tokenlist.d.ts +53 -0
  22. package/dist/server/internal/tokenlist.d.ts.map +1 -0
  23. package/dist/server/internal/tokenlist.js +38 -0
  24. package/dist/server/internal/tokenlist.js.map +1 -0
  25. package/package.json +1 -1
  26. package/src/core/Storage.ts +1 -1
  27. package/src/core/zod/rpc.ts +2 -0
  28. package/src/server/internal/handlers/exchange.ts +7 -31
  29. package/src/server/internal/handlers/relay.test.ts +61 -15
  30. package/src/server/internal/handlers/relay.ts +184 -94
  31. package/src/server/internal/kv.test.ts +129 -0
  32. package/src/server/internal/kv.ts +76 -10
  33. package/src/server/internal/tokenlist.ts +91 -0
@@ -0,0 +1,53 @@
1
+ import type { Address } from 'viem';
2
+ import type * as Kv from '../Kv.js';
3
+ /** A single tokenlist entry. */
4
+ export type Token = {
5
+ /** Token address. */
6
+ address: Address;
7
+ /** Token decimals. */
8
+ decimals: number;
9
+ /** Token logo URI. */
10
+ logoUri?: string | undefined;
11
+ /** Token name. */
12
+ name: string;
13
+ /** Token symbol. */
14
+ symbol: string;
15
+ };
16
+ /**
17
+ * Fetches the verified tokenlist for `chainId`. Reads through `kv` so
18
+ * concurrent callers in the same scope share a single upstream request,
19
+ * and so independent handlers (e.g. `Handler.relay` + `Handler.exchange`)
20
+ * reuse the same KV cache key.
21
+ *
22
+ * Returns an empty list on any non-OK response — callers should fall back
23
+ * to chain-supplied behavior rather than treating an empty list as fatal.
24
+ *
25
+ * @param chainId - Chain id to fetch the tokenlist for.
26
+ * @param kv - Kv used to cache responses across requests.
27
+ * @param options - Options.
28
+ * @returns Tokens for the chain.
29
+ */
30
+ export declare function fetch(chainId: number, kv: Kv.Kv, options?: fetch.Options): Promise<readonly Token[]>;
31
+ export declare namespace fetch {
32
+ /** Options for `fetch()`. */
33
+ type Options = {
34
+ /**
35
+ * Base URL of the tokenlist service.
36
+ * @default 'https://tokenlist.tempo.xyz'
37
+ */
38
+ baseUrl?: string | undefined;
39
+ /**
40
+ * TTL in seconds for the cached response.
41
+ * @default 600 (10 minutes)
42
+ */
43
+ cacheTtl?: number | undefined;
44
+ /**
45
+ * Override resolver. When provided, the cache hit path is unchanged but
46
+ * cache misses route through this function instead of fetching the
47
+ * default `tokenlist.tempo.xyz` URL. Useful for tests or for vendoring
48
+ * a curated list per app.
49
+ */
50
+ resolver?: ((chainId: number) => readonly Token[] | Promise<readonly Token[]>) | undefined;
51
+ };
52
+ }
53
+ //# sourceMappingURL=tokenlist.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokenlist.d.ts","sourceRoot":"","sources":["../../../src/server/internal/tokenlist.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAEnC,OAAO,KAAK,KAAK,EAAE,MAAM,UAAU,CAAA;AASnC,gCAAgC;AAChC,MAAM,MAAM,KAAK,GAAG;IAClB,qBAAqB;IACrB,OAAO,EAAE,OAAO,CAAA;IAChB,sBAAsB;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,sBAAsB;IACtB,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC5B,kBAAkB;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,oBAAoB;IACpB,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,KAAK,CACzB,OAAO,EAAE,MAAM,EACf,EAAE,EAAE,EAAE,CAAC,EAAE,EACT,OAAO,GAAE,KAAK,CAAC,OAAY,GAC1B,OAAO,CAAC,SAAS,KAAK,EAAE,CAAC,CAQ3B;AAED,MAAM,CAAC,OAAO,WAAW,KAAK,CAAC;IAC7B,6BAA6B;IAC7B,KAAK,OAAO,GAAG;QACb;;;WAGG;QACH,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;QAC5B;;;WAGG;QACH,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;QAC7B;;;;;WAKG;QACH,QAAQ,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,MAAM,KAAK,SAAS,KAAK,EAAE,GAAG,OAAO,CAAC,SAAS,KAAK,EAAE,CAAC,CAAC,GAAG,SAAS,CAAA;KAC3F,CAAA;CACF"}
@@ -0,0 +1,38 @@
1
+ import { cached } from './kv.js';
2
+ /** Default cache TTL in seconds (10 minutes) for tokenlist responses. */
3
+ const defaultCacheTtl = 10 * 60;
4
+ /** Default base URL for the verified tokenlist service. */
5
+ const defaultBaseUrl = 'https://tokenlist.tempo.xyz';
6
+ /**
7
+ * Fetches the verified tokenlist for `chainId`. Reads through `kv` so
8
+ * concurrent callers in the same scope share a single upstream request,
9
+ * and so independent handlers (e.g. `Handler.relay` + `Handler.exchange`)
10
+ * reuse the same KV cache key.
11
+ *
12
+ * Returns an empty list on any non-OK response — callers should fall back
13
+ * to chain-supplied behavior rather than treating an empty list as fatal.
14
+ *
15
+ * @param chainId - Chain id to fetch the tokenlist for.
16
+ * @param kv - Kv used to cache responses across requests.
17
+ * @param options - Options.
18
+ * @returns Tokens for the chain.
19
+ */
20
+ export async function fetch(chainId, kv, options = {}) {
21
+ const { baseUrl = defaultBaseUrl, cacheTtl = defaultCacheTtl, resolver } = options;
22
+ return cached(kv, `tokenlist:${chainId}`, async () => (resolver ? resolver(chainId) : fetchUncached(chainId, baseUrl)), { ttl: cacheTtl });
23
+ }
24
+ /**
25
+ * Default fetcher — resolves the verified tokenlist for `chainId` directly
26
+ * from `tokenlist.tempo.xyz`. Returns an empty list on any non-OK response.
27
+ */
28
+ async function fetchUncached(chainId, baseUrl) {
29
+ const response = await globalThis.fetch(`${baseUrl}/list/${chainId}`);
30
+ if (!response.ok)
31
+ return [];
32
+ const data = (await response.json());
33
+ return data.tokens.map(({ logoURI, ...token }) => ({
34
+ ...token,
35
+ logoUri: token.logoUri ?? logoURI,
36
+ }));
37
+ }
38
+ //# sourceMappingURL=tokenlist.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokenlist.js","sourceRoot":"","sources":["../../../src/server/internal/tokenlist.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAEhC,yEAAyE;AACzE,MAAM,eAAe,GAAG,EAAE,GAAG,EAAE,CAAA;AAE/B,2DAA2D;AAC3D,MAAM,cAAc,GAAG,6BAA6B,CAAA;AAgBpD;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,KAAK,CACzB,OAAe,EACf,EAAS,EACT,UAAyB,EAAE;IAE3B,MAAM,EAAE,OAAO,GAAG,cAAc,EAAE,QAAQ,GAAG,eAAe,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAA;IAClF,OAAO,MAAM,CACX,EAAE,EACF,aAAa,OAAO,EAAE,EACtB,KAAK,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,EAC5E,EAAE,GAAG,EAAE,QAAQ,EAAE,CAClB,CAAA;AACH,CAAC;AAyBD;;;GAGG;AACH,KAAK,UAAU,aAAa,CAAC,OAAe,EAAE,OAAe;IAC3D,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,KAAK,CAAC,GAAG,OAAO,SAAS,OAAO,EAAE,CAAC,CAAA;IACrE,IAAI,CAAC,QAAQ,CAAC,EAAE;QAAE,OAAO,EAAE,CAAA;IAC3B,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAElC,CAAA;IACD,OAAO,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;QACjD,GAAG,KAAK;QACR,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,OAAO;KAClC,CAAC,CAAC,CAAA;AACL,CAAC"}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "accounts",
3
3
  "description": "Tempo Accounts SDK",
4
4
  "type": "module",
5
- "version": "0.8.6",
5
+ "version": "0.8.8",
6
6
  "dependencies": {
7
7
  "hono": "^4.12.14",
8
8
  "idb-keyval": "^6.2.2",
@@ -37,7 +37,7 @@ export function combine(...storages: readonly Storage[]): Storage {
37
37
  return {
38
38
  async getItem<value>(name: string) {
39
39
  const results = await Promise.allSettled(storages.map((x) => x.getItem<value>(name)))
40
- const result = results.find((x) => x.status === 'fulfilled' && x.value !== null)
40
+ const result = results.find((x) => x.status === 'fulfilled' && x.value != null)
41
41
  if (result?.status !== 'fulfilled') return null
42
42
  return result.value as value
43
43
  },
@@ -551,6 +551,8 @@ export namespace wallet_send {
551
551
  method: z.literal('wallet_send'),
552
552
  params: z.optional(z.readonly(z.tuple([parameters]))),
553
553
  returns: z.object({
554
+ /** Chain id the send is to. */
555
+ chainId: u.number(),
554
556
  /** Receipt of the submitted send. */
555
557
  receipt,
556
558
  }),
@@ -19,6 +19,7 @@ import { type Handler, from } from '../../Handler.js'
19
19
  import * as Kv from '../../Kv.js'
20
20
  import * as Hono from '../hono.js'
21
21
  import { cached } from '../kv.js'
22
+ import * as Tokenlist from '../tokenlist.js'
22
23
 
23
24
  /** Default cache TTL in seconds (10 minutes). */
24
25
  const defaultCacheTtl = 10 * 60
@@ -144,11 +145,14 @@ export function exchange<const path extends string = '/exchange'>(
144
145
  kv = Kv.memory(),
145
146
  onRequest,
146
147
  path = '/exchange' as path,
147
- resolveTokens = defaultResolveTokens,
148
+ resolveTokens,
148
149
  transports = {},
149
150
  ...rest
150
151
  } = options
151
152
 
153
+ const getTokens = (chainId: number) =>
154
+ Tokenlist.fetch(chainId, kv, { cacheTtl, resolver: resolveTokens })
155
+
152
156
  const clients = new Map<number, Client>()
153
157
  for (const chain of chains) {
154
158
  const transport = transports[chain.id] ?? http()
@@ -170,12 +174,7 @@ export function exchange<const path extends string = '/exchange'>(
170
174
  const chain = chains.find((c) => c.id === chainId)
171
175
  if (!chain) throw new Error(`Chain ${chainId} is not supported.`)
172
176
 
173
- const tokens = await cached(
174
- kv,
175
- `tokenlist:${chain.id}`,
176
- async () => resolveTokens(chain.id),
177
- { ttl: cacheTtl },
178
- )
177
+ const tokens = await getTokens(chain.id)
179
178
 
180
179
  // Cache for `cacheTtl` and allow stale-while-revalidate for an
181
180
  // additional full TTL window so CDNs/browsers can serve immediately
@@ -202,12 +201,7 @@ export function exchange<const path extends string = '/exchange'>(
202
201
  const client = clients.get(chain.id)!
203
202
 
204
203
  // Resolve `token` and `pairToken` to addresses + metadata in parallel.
205
- const tokens = await cached(
206
- kv,
207
- `tokenlist:${chain.id}`,
208
- async () => resolveTokens(chain.id),
209
- { ttl: cacheTtl },
210
- )
204
+ const tokens = await getTokens(chain.id)
211
205
  const [tokenInfo, pairTokenInfo] = await Promise.all([
212
206
  resolveToken(client, { kv, ref: token, tokens }),
213
207
  resolveToken(client, { kv, ref: pairToken, tokens }),
@@ -478,21 +472,3 @@ function applySlippage(amount: bigint, slippage: number, dir: 'up' | 'down') {
478
472
  function toCall(call: { data: Hex.Hex; to: Address }) {
479
473
  return { data: call.data, to: call.to }
480
474
  }
481
-
482
- /**
483
- * Default `resolveTokens` implementation. Fetches the verified token list for
484
- * `chainId` from `https://tokenlist.tempo.xyz/list/:chainId`. Returns an empty
485
- * list on any non-OK response.
486
- */
487
- async function defaultResolveTokens(chainId: number): Promise<readonly Token[]> {
488
- // TODO: deprecate tokenlist.tempo.xyz when TIP-1026 enshrined.
489
- const response = await fetch(`https://tokenlist.tempo.xyz/list/${chainId}`)
490
- if (!response.ok) return []
491
- const data = (await response.json()) as {
492
- tokens: readonly (Token & { logoURI?: string })[]
493
- }
494
- return data.tokens.map(({ logoURI, ...token }) => ({
495
- ...token,
496
- logoUri: token.logoUri ?? logoURI,
497
- }))
498
- }
@@ -14,6 +14,18 @@ const userAccount = accounts[9]!
14
14
  const feePayerAccount = accounts[0]!
15
15
  const recipient = accounts[7]!
16
16
 
17
+ /**
18
+ * Tokens the relay handler probes for fee-token resolution. The default
19
+ * `resolveTokens` fetches `tokenlist.tempo.xyz`, which doesn't know about
20
+ * the localnet chain — so tests inject this list explicitly.
21
+ */
22
+ const localnetTokens = [
23
+ { address: '0x20c0000000000000000000000000000000000000', decimals: 6, name: 'pathUSD', symbol: 'pathUSD' },
24
+ { address: '0x20c0000000000000000000000000000000000001', decimals: 6, name: 'alphaUSD', symbol: 'alphaUSD' },
25
+ { address: '0x20c0000000000000000000000000000000000002', decimals: 6, name: 'betaUSD', symbol: 'betaUSD' },
26
+ { address: '0x20c0000000000000000000000000000000000003', decimals: 6, name: 'thetaUSD', symbol: 'thetaUSD' },
27
+ ] as const
28
+
17
29
  /** Case-insensitive lookup into balanceDiffs keyed by address. */
18
30
  function findDiffs(
19
31
  balanceDiffs: Capabilities.FillTransactionCapabilities['balanceDiffs'],
@@ -1110,6 +1122,7 @@ describe('behavior: path A — guaranteed sponsorship (no validate)', () => {
1110
1122
  relay({
1111
1123
  chains: [chain],
1112
1124
  features: 'all',
1125
+ resolveTokens: () => localnetTokens,
1113
1126
  transports: { [chain.id]: http() },
1114
1127
  feePayer: {
1115
1128
  account: feePayerAccount,
@@ -1335,6 +1348,7 @@ describe('behavior: fee token resolution', () => {
1335
1348
  relay({
1336
1349
  chains: [chain],
1337
1350
  features: 'all',
1351
+ resolveTokens: () => localnetTokens,
1338
1352
  transports: { [chain.id]: http() },
1339
1353
  }).listener,
1340
1354
  )
@@ -1366,30 +1380,62 @@ describe('behavior: fee token resolution', () => {
1366
1380
  })
1367
1381
 
1368
1382
  test('behavior: resolves to highest-balance token from token list', async () => {
1369
- // Mint different amounts of two tokens to a fresh account.
1383
+ // Create a fresh TIP20 token "betaUsd" so we can test highest-balance
1384
+ // selection without depending on a pre-deployed second token.
1370
1385
  const freshAccount = accounts[5]!
1371
- const betaUsd = '0x20c0000000000000000000000000000000000002'
1372
- const rpc = getClient()
1373
- await Actions.token.mintSync(rpc, {
1374
- account: accounts[0]!,
1375
- token: addresses.alphaUsd,
1376
- amount: 100n,
1377
- to: freshAccount.address,
1386
+ const rpc = getClient({ account: accounts[0]! })
1387
+ const { token: betaUsd } = await Actions.token.createSync(rpc, {
1388
+ name: 'BetaUSD',
1389
+ symbol: 'BetaUSD',
1390
+ currency: 'USD',
1391
+ quoteToken: addresses.alphaUsd,
1378
1392
  })
1379
- await Actions.token.mintSync(rpc, {
1380
- account: accounts[0]!,
1381
- token: betaUsd,
1382
- amount: 500n,
1383
- to: freshAccount.address,
1393
+ await sendTransactionSync(rpc, {
1394
+ calls: [
1395
+ Actions.token.grantRoles.call({
1396
+ token: betaUsd,
1397
+ role: 'issuer',
1398
+ to: rpc.account!.address,
1399
+ }),
1400
+ // Mint different amounts of two tokens to a fresh account. Amounts
1401
+ // must be large enough to cover gas, otherwise autoSwap fires.
1402
+ Actions.token.mint.call({
1403
+ token: addresses.alphaUsd,
1404
+ amount: parseUnits('100', 6),
1405
+ to: freshAccount.address,
1406
+ }),
1407
+ Actions.token.mint.call({
1408
+ token: betaUsd,
1409
+ amount: parseUnits('500', 6),
1410
+ to: freshAccount.address,
1411
+ }),
1412
+ ],
1384
1413
  })
1385
1414
 
1386
- const { transaction } = await fillTransaction(client, {
1415
+ // Spin up a relay whose tokenlist includes the freshly created betaUsd
1416
+ // alongside the localnet defaults so the highest-balance resolver can
1417
+ // see both.
1418
+ const customServer = await createServer(
1419
+ relay({
1420
+ chains: [chain],
1421
+ features: 'all',
1422
+ resolveTokens: () => [
1423
+ ...localnetTokens,
1424
+ { address: betaUsd, decimals: 6, name: 'BetaUSD', symbol: 'BetaUSD' },
1425
+ ],
1426
+ transports: { [chain.id]: http() },
1427
+ }).listener,
1428
+ )
1429
+ const customClient = getClient({ transport: http(customServer.url) })
1430
+
1431
+ const { transaction } = await fillTransaction(customClient, {
1387
1432
  account: freshAccount.address,
1388
1433
  calls: [transferCall()],
1389
1434
  })
1435
+ customServer.close()
1390
1436
 
1391
1437
  // betaUsd has higher balance (500 > 100).
1392
- expect(transaction.feeToken).toBe(betaUsd)
1438
+ expect(transaction.feeToken?.toLowerCase()).toBe(betaUsd.toLowerCase())
1393
1439
  })
1394
1440
 
1395
1441
  test('behavior: falls back to pathUSD when no preference or balances', async () => {