tapimo 0.0.2 → 0.1.0

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 (265) hide show
  1. package/README.md +11 -2
  2. package/dist/ApiKey.d.ts +12 -11
  3. package/dist/ApiKey.d.ts.map +1 -1
  4. package/dist/ApiKey.js +10 -12
  5. package/dist/ApiKey.js.map +1 -1
  6. package/dist/ApiKeys.d.ts +139 -0
  7. package/dist/ApiKeys.d.ts.map +1 -0
  8. package/dist/ApiKeys.js +166 -0
  9. package/dist/ApiKeys.js.map +1 -0
  10. package/dist/App.d.ts +920 -910
  11. package/dist/App.d.ts.map +1 -1
  12. package/dist/App.js +36 -12
  13. package/dist/App.js.map +1 -1
  14. package/dist/Handler.d.ts +86 -0
  15. package/dist/Handler.d.ts.map +1 -0
  16. package/dist/Handler.js +83 -0
  17. package/dist/Handler.js.map +1 -0
  18. package/dist/Scope.d.ts +4 -1
  19. package/dist/Scope.d.ts.map +1 -1
  20. package/dist/Scope.js +5 -1
  21. package/dist/Scope.js.map +1 -1
  22. package/dist/Webhooks.d.ts +3 -5
  23. package/dist/Webhooks.d.ts.map +1 -1
  24. package/dist/Webhooks.js +9 -2
  25. package/dist/Webhooks.js.map +1 -1
  26. package/dist/admin/Access.d.ts +1 -3
  27. package/dist/admin/Access.d.ts.map +1 -1
  28. package/dist/admin/Access.js +1 -3
  29. package/dist/admin/Access.js.map +1 -1
  30. package/dist/admin/App.d.ts +98 -209
  31. package/dist/admin/App.d.ts.map +1 -1
  32. package/dist/admin/App.js +2 -4
  33. package/dist/admin/App.js.map +1 -1
  34. package/dist/admin/apps/api-keys.d.ts +7 -5
  35. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  36. package/dist/admin/apps/api-keys.js +8 -9
  37. package/dist/admin/apps/api-keys.js.map +1 -1
  38. package/dist/admin/apps/scopes.d.ts +3 -0
  39. package/dist/admin/apps/scopes.d.ts.map +1 -1
  40. package/dist/admin/apps/verified-tokens.d.ts +101 -215
  41. package/dist/admin/apps/verified-tokens.d.ts.map +1 -1
  42. package/dist/admin/apps/verified-tokens.js +17 -38
  43. package/dist/admin/apps/verified-tokens.js.map +1 -1
  44. package/dist/admin/ui.generated.d.ts +1 -1
  45. package/dist/admin/ui.generated.js +1 -1
  46. package/dist/apps/balances.d.ts +2 -2
  47. package/dist/apps/receipts.d.ts +43 -43
  48. package/dist/apps/tokens.d.ts +454 -454
  49. package/dist/apps/transactions.d.ts +43 -43
  50. package/dist/apps/transfers.d.ts +29 -29
  51. package/dist/apps/webhooks.d.ts +5 -2
  52. package/dist/apps/webhooks.d.ts.map +1 -1
  53. package/dist/apps/webhooks.js +38 -34
  54. package/dist/apps/webhooks.js.map +1 -1
  55. package/dist/{Cli.d.ts → cli/Cli.d.ts} +13 -2
  56. package/dist/cli/Cli.d.ts.map +1 -0
  57. package/dist/cli/Cli.js +19 -0
  58. package/dist/cli/Cli.js.map +1 -0
  59. package/dist/cli/bin.d.ts +3 -0
  60. package/dist/cli/bin.d.ts.map +1 -0
  61. package/dist/cli/bin.js +4 -0
  62. package/dist/cli/bin.js.map +1 -0
  63. package/dist/cli/commands/admin.d.ts +15 -0
  64. package/dist/cli/commands/admin.d.ts.map +1 -0
  65. package/dist/cli/commands/admin.js +60 -0
  66. package/dist/cli/commands/admin.js.map +1 -0
  67. package/dist/cloudflare.d.ts +7 -0
  68. package/dist/cloudflare.d.ts.map +1 -1
  69. package/dist/cloudflare.js +9 -0
  70. package/dist/cloudflare.js.map +1 -1
  71. package/dist/db/Db.d.ts +103 -0
  72. package/dist/db/Db.d.ts.map +1 -0
  73. package/dist/db/Db.js +274 -0
  74. package/dist/db/Db.js.map +1 -0
  75. package/dist/db/Schema.d.ts +88 -0
  76. package/dist/db/Schema.d.ts.map +1 -0
  77. package/dist/db/Schema.js +130 -0
  78. package/dist/db/Schema.js.map +1 -0
  79. package/dist/db/tables/organizations.d.ts +79 -0
  80. package/dist/db/tables/organizations.d.ts.map +1 -0
  81. package/dist/db/tables/organizations.js +81 -0
  82. package/dist/db/tables/organizations.js.map +1 -0
  83. package/dist/db/tables/verifiedTokens.d.ts +53 -0
  84. package/dist/db/tables/verifiedTokens.d.ts.map +1 -0
  85. package/dist/db/tables/verifiedTokens.js +74 -0
  86. package/dist/db/tables/verifiedTokens.js.map +1 -0
  87. package/dist/db/tables/webhookDeliveries.d.ts +71 -0
  88. package/dist/db/tables/webhookDeliveries.d.ts.map +1 -0
  89. package/dist/db/tables/webhookDeliveries.js +119 -0
  90. package/dist/db/tables/webhookDeliveries.js.map +1 -0
  91. package/dist/db/tables/webhookSubscriptions.d.ts +158 -0
  92. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -0
  93. package/dist/db/tables/webhookSubscriptions.js +272 -0
  94. package/dist/db/tables/webhookSubscriptions.js.map +1 -0
  95. package/dist/handlers/internal/executionError.d.ts +25 -0
  96. package/dist/handlers/internal/executionError.d.ts.map +1 -0
  97. package/dist/handlers/internal/executionError.js +184 -0
  98. package/dist/handlers/internal/executionError.js.map +1 -0
  99. package/dist/handlers/internal/multisig.d.ts +237 -0
  100. package/dist/handlers/internal/multisig.d.ts.map +1 -0
  101. package/dist/handlers/internal/multisig.js +567 -0
  102. package/dist/handlers/internal/multisig.js.map +1 -0
  103. package/dist/handlers/internal/requestListener.d.ts +124 -0
  104. package/dist/handlers/internal/requestListener.d.ts.map +1 -0
  105. package/dist/handlers/internal/requestListener.js +173 -0
  106. package/dist/handlers/internal/requestListener.js.map +1 -0
  107. package/dist/handlers/internal/sponsorship.d.ts +83 -0
  108. package/dist/handlers/internal/sponsorship.d.ts.map +1 -0
  109. package/dist/handlers/internal/sponsorship.js +103 -0
  110. package/dist/handlers/internal/sponsorship.js.map +1 -0
  111. package/dist/handlers/internal/types.d.ts +284 -0
  112. package/dist/handlers/internal/types.d.ts.map +1 -0
  113. package/dist/handlers/internal/types.js +2 -0
  114. package/dist/handlers/internal/types.js.map +1 -0
  115. package/dist/handlers/internal/utils.d.ts +34 -0
  116. package/dist/handlers/internal/utils.d.ts.map +1 -0
  117. package/dist/handlers/internal/utils.js +131 -0
  118. package/dist/handlers/internal/utils.js.map +1 -0
  119. package/dist/handlers/relay.d.ts +119 -0
  120. package/dist/handlers/relay.d.ts.map +1 -0
  121. package/dist/handlers/relay.js +1264 -0
  122. package/dist/handlers/relay.js.map +1 -0
  123. package/dist/index.d.ts +4 -2
  124. package/dist/index.d.ts.map +1 -1
  125. package/dist/index.js +4 -2
  126. package/dist/index.js.map +1 -1
  127. package/dist/internal/Auth.d.ts +16 -10
  128. package/dist/internal/Auth.d.ts.map +1 -1
  129. package/dist/internal/Auth.js +72 -12
  130. package/dist/internal/Auth.js.map +1 -1
  131. package/dist/internal/RpcSchema.d.ts +10 -0
  132. package/dist/internal/RpcSchema.d.ts.map +1 -0
  133. package/dist/internal/RpcSchema.js +2 -0
  134. package/dist/internal/RpcSchema.js.map +1 -0
  135. package/dist/internal/Store.d.ts +42 -3
  136. package/dist/internal/Store.d.ts.map +1 -1
  137. package/dist/internal/Store.js +65 -3
  138. package/dist/internal/Store.js.map +1 -1
  139. package/dist/internal/VerifiedTokens.d.ts +15 -34
  140. package/dist/internal/VerifiedTokens.d.ts.map +1 -1
  141. package/dist/internal/VerifiedTokens.js +115 -159
  142. package/dist/internal/VerifiedTokens.js.map +1 -1
  143. package/dist/internal/Webhooks.d.ts +49 -35
  144. package/dist/internal/Webhooks.d.ts.map +1 -1
  145. package/dist/internal/Webhooks.js +92 -205
  146. package/dist/internal/Webhooks.js.map +1 -1
  147. package/package.json +7 -2
  148. package/src/ApiKey.test.ts +3 -3
  149. package/src/ApiKey.ts +10 -12
  150. package/src/ApiKeys.test.ts +175 -0
  151. package/src/ApiKeys.ts +205 -0
  152. package/src/App.test-d.ts +1 -1
  153. package/src/App.test.ts +10768 -351
  154. package/src/App.ts +79 -36
  155. package/src/Client.test-d.ts +1 -1
  156. package/src/Handler.ts +154 -0
  157. package/src/Scope.ts +5 -1
  158. package/src/Webhooks.test.ts +8 -7
  159. package/src/Webhooks.ts +11 -5
  160. package/src/admin/Access.ts +1 -3
  161. package/src/admin/App.ts +19 -24
  162. package/src/admin/apps/api-keys.test.ts +19 -18
  163. package/src/admin/apps/api-keys.ts +8 -9
  164. package/src/admin/apps/scopes.test.ts +4 -0
  165. package/src/admin/apps/verified-tokens.test.ts +3 -30
  166. package/src/admin/apps/verified-tokens.ts +17 -36
  167. package/src/admin/ui.generated.ts +1 -1
  168. package/src/apps/balances.test.ts +4 -3
  169. package/src/apps/blocks.test.ts +1 -1
  170. package/src/apps/exchanges.test.ts +1 -1
  171. package/src/apps/indexer.test.ts +1 -1
  172. package/src/apps/receipts.test.ts +1 -1
  173. package/src/apps/rpc.test.ts +100 -10
  174. package/src/apps/tokenlist.test.ts +5 -7
  175. package/src/apps/tokens.test.ts +4 -3
  176. package/src/apps/transactions.test.ts +1 -1
  177. package/src/apps/transfers.test.ts +1 -1
  178. package/src/apps/verified-tokens.test.ts +24 -12
  179. package/src/apps/webhooks.test.ts +130 -143
  180. package/src/apps/webhooks.ts +45 -47
  181. package/src/{Cli.test.ts → cli/Cli.test.ts} +10 -9
  182. package/src/{Cli.ts → cli/Cli.ts} +16 -9
  183. package/src/cli/bin.ts +4 -0
  184. package/src/cli/commands/admin.test.ts +71 -0
  185. package/src/cli/commands/admin.ts +60 -0
  186. package/src/cloudflare.ts +10 -0
  187. package/src/db/Db.test.ts +84 -0
  188. package/src/db/Db.ts +383 -0
  189. package/src/db/Schema.ts +164 -0
  190. package/src/db/tables/organizations.test.ts +124 -0
  191. package/src/db/tables/organizations.ts +127 -0
  192. package/src/db/tables/verifiedTokens.ts +111 -0
  193. package/src/db/tables/webhookDeliveries.ts +159 -0
  194. package/src/db/tables/webhookSubscriptions.ts +358 -0
  195. package/src/handlers/internal/executionError.ts +205 -0
  196. package/src/handlers/internal/multisig.localnet.test.ts +146 -0
  197. package/src/handlers/internal/multisig.test.ts +744 -0
  198. package/src/handlers/internal/multisig.ts +858 -0
  199. package/src/handlers/internal/relay.localnet.test.ts +2035 -0
  200. package/src/handlers/internal/requestListener.ts +273 -0
  201. package/src/handlers/internal/sponsorship.ts +186 -0
  202. package/src/handlers/internal/types.ts +378 -0
  203. package/src/handlers/internal/utils.ts +153 -0
  204. package/src/handlers/relay.ts +1605 -0
  205. package/src/index.ts +4 -2
  206. package/src/internal/Auth.test.ts +21 -21
  207. package/src/internal/Auth.ts +92 -19
  208. package/src/internal/RateLimit.test.ts +1 -1
  209. package/src/internal/RpcSchema.ts +10 -0
  210. package/src/internal/Store.test.ts +98 -10
  211. package/src/internal/Store.ts +112 -6
  212. package/src/internal/VerifiedTokens.test.ts +99 -115
  213. package/src/internal/VerifiedTokens.ts +123 -175
  214. package/src/internal/Webhooks.test.ts +107 -107
  215. package/src/internal/Webhooks.ts +102 -220
  216. package/dist/ApiKeySource.d.ts +0 -166
  217. package/dist/ApiKeySource.d.ts.map +0 -1
  218. package/dist/ApiKeySource.js +0 -234
  219. package/dist/ApiKeySource.js.map +0 -1
  220. package/dist/Cli.d.ts.map +0 -1
  221. package/dist/Cli.js +0 -13
  222. package/dist/Cli.js.map +0 -1
  223. package/seeds/31318/icons/0x20c0000000000000000000000000000000000000.svg +0 -1
  224. package/seeds/31318/icons/0x20c0000000000000000000000000000000000001.svg +0 -1
  225. package/seeds/31318/icons/0x20c0000000000000000000000000000000000002.svg +0 -1
  226. package/seeds/31318/icons/0x20c0000000000000000000000000000000000003.svg +0 -1
  227. package/seeds/31318/verified-tokens.json +0 -30
  228. package/seeds/4217/icons/0x20c0000000000000000000000000000000000000.svg +0 -25
  229. package/seeds/4217/icons/0x20c000000000000000000000048c8f36df1c9a4a.svg +0 -1
  230. package/seeds/4217/icons/0x20c0000000000000000000000520792dcccccccc.svg +0 -1
  231. package/seeds/4217/icons/0x20c0000000000000000000000a6da882d075a4c3.svg +0 -4
  232. package/seeds/4217/icons/0x20c000000000000000000000111111111e910f0f.svg +0 -30
  233. package/seeds/4217/icons/0x20c00000000000000000000014f22ca97301eb73.svg +0 -1
  234. package/seeds/4217/icons/0x20c0000000000000000000001621e21f71cf12fb.svg +0 -1
  235. package/seeds/4217/icons/0x20c0000000000000000000002f52d5cc21a3207b.svg +0 -1
  236. package/seeds/4217/icons/0x20c0000000000000000000003158081efd85bfc2.svg +0 -11
  237. package/seeds/4217/icons/0x20c0000000000000000000003554d28269e0f3c2.svg +0 -1
  238. package/seeds/4217/icons/0x20c000000000000000000000383a23bacb546ab9.svg +0 -1
  239. package/seeds/4217/icons/0x20c00000000000000000000042109aef2f8b28e1.png +0 -0
  240. package/seeds/4217/icons/0x20c0000000000000000000005c0bac7cef389a11.svg +0 -1
  241. package/seeds/4217/icons/0x20c0000000000000000000006fd9a167923ba194.png +0 -0
  242. package/seeds/4217/icons/0x20c0000000000000000000007f7ba549dd0251b9.svg +0 -1
  243. package/seeds/4217/icons/0x20c0000000000000000000008191667423f70e67.svg +0 -5
  244. package/seeds/4217/icons/0x20c0000000000000000000008ee4fcff88888888.svg +0 -1
  245. package/seeds/4217/icons/0x20c0000000000000000000009a4a4b17e0dc6651.svg +0 -1
  246. package/seeds/4217/icons/0x20c000000000000000000000ab02d39df30bd17e.svg +0 -1
  247. package/seeds/4217/icons/0x20c000000000000000000000ae247a1130450f09.svg +0 -1
  248. package/seeds/4217/icons/0x20c000000000000000000000aeed2ec36a54d0e5.svg +0 -1
  249. package/seeds/4217/icons/0x20c000000000000000000000b9537d11c60e8b50.svg +0 -1
  250. package/seeds/4217/icons/0x20c000000000000000000000bd95bfb69fbe6ce3.svg +0 -1
  251. package/seeds/4217/icons/0x20c000000000000000000000c412ec89d0c08be5.svg +0 -1
  252. package/seeds/4217/icons/0x20c000000000000000000000f047dd7018e50367.png +0 -0
  253. package/seeds/4217/verified-tokens.json +0 -177
  254. package/seeds/42431/icons/0x20c0000000000000000000000000000000000000.svg +0 -25
  255. package/seeds/42431/icons/0x20c0000000000000000000000000000000000001.svg +0 -1
  256. package/seeds/42431/icons/0x20c0000000000000000000000000000000000002.svg +0 -1
  257. package/seeds/42431/icons/0x20c0000000000000000000000000000000000003.svg +0 -1
  258. package/seeds/42431/icons/0x20c0000000000000000000009e8d7eb59b783726.svg +0 -1
  259. package/seeds/42431/icons/0x20c000000000000000000000d72572838bbee59c.svg +0 -1
  260. package/seeds/42431/verified-tokens.json +0 -44
  261. package/seeds/openrpc/README.md +0 -27
  262. package/seeds/openrpc/execution-apis.json +0 -8989
  263. package/src/ApiKeySource.test-d.ts +0 -11
  264. package/src/ApiKeySource.test.ts +0 -295
  265. package/src/ApiKeySource.ts +0 -303
@@ -0,0 +1,1264 @@
1
+ import { AbiEvent, Hex, RpcRequest, RpcResponse } from 'ox';
2
+ import { Transaction as core_Transaction } from 'ox/tempo';
3
+ import { createClient, decodeFunctionData, formatUnits, http, isAddress, parseEventLogs, zeroAddress, } from 'viem';
4
+ import { simulateCalls } from 'viem/actions';
5
+ import { Abis, Actions, Addresses, Capabilities, Transaction, VirtualAddress } from 'viem/tempo';
6
+ import * as ExecutionError from './internal/executionError.js';
7
+ import { from } from '../Handler.js';
8
+ import * as Store from '../internal/Store.js';
9
+ import * as Multisig from './internal/multisig.js';
10
+ import * as Sponsorship from './internal/sponsorship.js';
11
+ import * as Utils from './internal/utils.js';
12
+ /**
13
+ * Instantiates a relay handler that proxies `eth_fillTransaction`
14
+ * with wallet-aware enrichment (fee token resolution, simulation,
15
+ * sponsorship, AMM resolution).
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * import { Handler } from 'tempo-api/server'
20
+ *
21
+ * const handler = Handler.relay({ getClient })
22
+ *
23
+ * // Plug handler into your server framework of choice:
24
+ * createServer(handler.listener) // Node.js
25
+ * Bun.serve(handler) // Bun
26
+ * Deno.serve(handler) // Deno
27
+ * app.use(handler.listener) // Express
28
+ * app.all('*', c => handler.fetch(c.req.raw)) // Hono
29
+ * export const GET = handler.fetch // Next.js
30
+ * export const POST = handler.fetch // Next.js
31
+ * ```
32
+ *
33
+ * @example
34
+ * With sponsorship
35
+ *
36
+ * ```ts
37
+ * import { privateKeyToAccount } from 'viem/accounts'
38
+ * import { Handler } from 'tempo-api/server'
39
+ *
40
+ * const handler = Handler.relay({
41
+ * getClient,
42
+ * feePayer: {
43
+ * account: privateKeyToAccount('0x...'),
44
+ * },
45
+ * })
46
+ * ```
47
+ *
48
+ * @param options - Options.
49
+ * @returns Request handler.
50
+ */
51
+ export function relay(options) {
52
+ const { getClient, internal_allowUnsafeUrls = false, onRequest, path = '/', resolveTokens, ...rest } = options;
53
+ const feePayerOptions = options.feePayer;
54
+ // Adapt the state store into the multisig operation store once, up front.
55
+ const multisigOptions = options.multisig
56
+ ? { ...options.multisig, store: Multisig.fromStore(options.multisig.store) }
57
+ : undefined;
58
+ // Resolves the chain's fee-token candidates through the caller-provided
59
+ // resolver. An unconfigured resolver (or a chain it has no list for) yields
60
+ // an empty candidate set.
61
+ const getTokens = async (chainId) => {
62
+ if (!resolveTokens || chainId === undefined)
63
+ return [];
64
+ return await resolveTokens(chainId);
65
+ };
66
+ // Memoize cache for TIP-20 metadata and user fee-token preferences. Kept
67
+ // opt-in: an always-on cache would pin a raced null preference (set, then
68
+ // fill before the set mines) for the whole TTL.
69
+ const store = options.cache;
70
+ const features = {
71
+ autoSwap: options.autoSwap ?? options.features === 'all',
72
+ feeTokenResolution: options.features === 'all' || Boolean(resolveTokens),
73
+ simulate: options.features === 'all',
74
+ };
75
+ const autoSwap = (() => {
76
+ if (!features.autoSwap)
77
+ return undefined;
78
+ if (options.autoSwap === false)
79
+ return undefined;
80
+ return {
81
+ slippage: (typeof options.autoSwap === 'object' ? options.autoSwap?.slippage : undefined) ?? 0.05,
82
+ };
83
+ })();
84
+ async function handleRequest(request, options) {
85
+ await onRequest?.(request);
86
+ // Resolve chainId from: 1) explicit option (URL path), 2) first param object, 3) default chain.
87
+ const params = 'params' in request && Array.isArray(request.params) ? request.params : [];
88
+ const first = typeof params[0] === 'object' && params[0]
89
+ ? params[0]
90
+ : undefined;
91
+ const chainId = Utils.resolveChainId(first?.['chainId']) ?? options?.chainId ?? getClient().chain?.id;
92
+ const client = getClient(chainId);
93
+ switch (request.method) {
94
+ case 'eth_fillTransaction': {
95
+ const parameters = params[0];
96
+ const capabilities = (parameters['capabilities'] ?? {});
97
+ try {
98
+ const from = typeof parameters['from'] === 'string' ? parameters['from'] : undefined;
99
+ const requestFeeToken = typeof parameters['feeToken'] === 'string'
100
+ ? parameters['feeToken']
101
+ : undefined;
102
+ const externalFeePayerUrl = typeof parameters['feePayer'] === 'string'
103
+ ? normalizeExternalFeePayerUrl(parameters['feePayer'], {
104
+ allowUnsafe: internal_allowUnsafeUrls,
105
+ })
106
+ : undefined;
107
+ const requestsSponsorship = (!!feePayerOptions || !!externalFeePayerUrl) && parameters['feePayer'] !== false;
108
+ // Default to `true`. Dapps that don't render diffs can pass
109
+ // `capabilities.balanceDiffs: false` to skip the post-fill
110
+ // `tempo_simulateV1` round trip (~250-400ms).
111
+ const requireBalanceDiffs = capabilities['balanceDiffs'] !== false;
112
+ const { feePayer: _feePayer, ...normalized } = Utils.normalizeFillTransactionRequest(parameters);
113
+ // When sponsoring, the sponsor's preferred fee token (if set)
114
+ // overrides whatever the consumer asked for — the sponsor is
115
+ // paying, so the sponsor picks. Falls back to the request's
116
+ // feeToken otherwise.
117
+ const sponsoredFeeToken = requestsSponsorship
118
+ ? (feePayerOptions?.feeToken ?? requestFeeToken)
119
+ : requestFeeToken;
120
+ const baseTx = {
121
+ ...normalized,
122
+ ...(typeof chainId !== 'undefined' ? { chainId } : {}),
123
+ ...(sponsoredFeeToken ? { feeToken: sponsoredFeeToken } : {}),
124
+ };
125
+ let filled;
126
+ let sponsored = false;
127
+ let feeToken = sponsoredFeeToken;
128
+ // Lazily resolve a swap source token when autoSwap needs one.
129
+ const resolveFeeTokenForSwap = from
130
+ ? async (insufficientToken) => resolveFeeToken(client, {
131
+ account: from,
132
+ feeToken: undefined,
133
+ store,
134
+ tokens: (await getTokens(chainId)).filter((t) => t.toLowerCase() !== insufficientToken.toLowerCase()),
135
+ })
136
+ : undefined;
137
+ // When no sponsor will pay, prefer fee tokens the user actually
138
+ // holds. Extend the configured token list with any TIP20 token
139
+ // the transaction is calling — typically the token being
140
+ // transferred — so a user transferring USDC.e can pay gas in
141
+ // USDC.e even when the configured list defaults to pathUSD.
142
+ const configuredTokens = await getTokens(chainId);
143
+ const unsponsoredTokens = [
144
+ ...configuredTokens,
145
+ ...callTargetTokens(baseTx).filter((t) => !configuredTokens.some((rt) => rt.toLowerCase() === t.toLowerCase())),
146
+ ];
147
+ // When the app provides its own fee payer URL, route the fill
148
+ // through that service so it can sign the transaction.
149
+ const fillClient = externalFeePayerUrl
150
+ ? createClient({
151
+ chain: client.chain,
152
+ batch: { multicall: { deployless: true } },
153
+ transport: http(externalFeePayerUrl),
154
+ })
155
+ : client;
156
+ if (requestsSponsorship && (externalFeePayerUrl || !feePayerOptions?.validate)) {
157
+ // Path A: sponsorship guaranteed — skip fee token resolution,
158
+ // fill once with feePayer, then parallelize the rest. Taken when:
159
+ // - no validate is configured (no gating), OR
160
+ // - the app supplied its own external fee payer URL (that
161
+ // relay is the sponsorship authority — the wallet's own
162
+ // `validate` only governs the wallet's own fee payer).
163
+ // Default to the chain's first token so the broadcast envelope
164
+ // always carries a feeToken the chain can charge.
165
+ if (!feeToken)
166
+ feeToken = (await getTokens(chainId))[0];
167
+ const transaction = {
168
+ ...baseTx,
169
+ feePayer: true,
170
+ ...(feeToken ? { feeToken } : {}),
171
+ };
172
+ if (Sponsorship.isPreparedTransaction(transaction)) {
173
+ filled = {
174
+ transaction: Utils.normalizeTempoTransaction(transaction),
175
+ sponsor: undefined,
176
+ };
177
+ }
178
+ else {
179
+ filled = await fill(fillClient, {
180
+ autoSwap,
181
+ feeToken,
182
+ store,
183
+ resolveFeeToken: resolveFeeTokenForSwap,
184
+ transaction,
185
+ });
186
+ // The chain echoes feeToken: null for sponsored fills served
187
+ // by viem clients that strip feeToken pre-sign; re-inject
188
+ // before the spread in mergeCallsFromRequest drops it.
189
+ if (feeToken &&
190
+ filled?.transaction &&
191
+ filled.transaction.feeToken == null)
192
+ filled.transaction.feeToken = feeToken;
193
+ }
194
+ sponsored = true;
195
+ }
196
+ else if (requestsSponsorship && feePayerOptions?.validate) {
197
+ // Path B: sponsorship possible but may be rejected.
198
+ // Speculatively fill the sponsored variant, validate, and only
199
+ // fall back to the unsponsored path on rejection. The accept
200
+ // case (common) avoids the upfront `resolveFeeToken` round
201
+ // trips and the second `eth_fillTransaction`.
202
+ const sponsoredTx = { ...baseTx, feePayer: true };
203
+ if (Sponsorship.isPreparedTransaction(sponsoredTx)) {
204
+ // Already prepared — skip fills, just validate sponsorship.
205
+ const prepared = {
206
+ transaction: Utils.normalizeTempoTransaction(sponsoredTx),
207
+ sponsor: undefined,
208
+ };
209
+ sponsored = await Sponsorship.shouldSponsor({
210
+ sender: from,
211
+ transaction: prepared.transaction,
212
+ validate: feePayerOptions.validate,
213
+ });
214
+ filled = prepared;
215
+ }
216
+ else {
217
+ const options = {
218
+ autoSwap,
219
+ store,
220
+ resolveFeeToken: resolveFeeTokenForSwap,
221
+ };
222
+ const fill_sponsored = await fill(fillClient, {
223
+ ...options,
224
+ feeToken,
225
+ transaction: sponsoredTx,
226
+ });
227
+ sponsored = await Sponsorship.shouldSponsor({
228
+ sender: from,
229
+ transaction: fill_sponsored.transaction,
230
+ validate: feePayerOptions.validate,
231
+ });
232
+ if (sponsored) {
233
+ filled = fill_sponsored;
234
+ }
235
+ else {
236
+ // Sponsor rejected — resolve fee token and fill unsponsored.
237
+ const feeToken_unsponsored = features.feeTokenResolution
238
+ ? await resolveFeeToken(client, {
239
+ account: from,
240
+ feeToken: requestFeeToken,
241
+ store,
242
+ tokens: unsponsoredTokens,
243
+ })
244
+ : requestFeeToken;
245
+ const tx_unsponsored = {
246
+ ...baseTx,
247
+ ...(feeToken_unsponsored ? { feeToken: feeToken_unsponsored } : {}),
248
+ };
249
+ filled = await fill(client, {
250
+ ...options,
251
+ feeToken: feeToken_unsponsored,
252
+ transaction: tx_unsponsored,
253
+ });
254
+ feeToken = feeToken_unsponsored;
255
+ }
256
+ }
257
+ }
258
+ else {
259
+ // Path C: no sponsorship configured — resolve fee token, fill once.
260
+ feeToken = features.feeTokenResolution
261
+ ? await resolveFeeToken(client, {
262
+ account: from,
263
+ feeToken: requestFeeToken,
264
+ store,
265
+ tokens: unsponsoredTokens,
266
+ })
267
+ : requestFeeToken;
268
+ const transaction = { ...baseTx, ...(feeToken ? { feeToken } : {}) };
269
+ filled = await fill(client, {
270
+ autoSwap,
271
+ feeToken,
272
+ store,
273
+ resolveFeeToken: resolveFeeTokenForSwap,
274
+ transaction,
275
+ });
276
+ }
277
+ const transaction_filled = filled.transaction;
278
+ const swap = 'swap' in filled ? filled.swap : undefined;
279
+ if (!feeToken)
280
+ feeToken =
281
+ transaction_filled.feeToken ?? (await getTokens(chainId))[0];
282
+ // Parallelize: simulate, fee payer signing, and autoSwap metadata.
283
+ const alreadySigned = 'feePayerSignature' in transaction_filled &&
284
+ transaction_filled.feePayerSignature != null;
285
+ const calls = extractCalls(transaction_filled);
286
+ const [simulation, transaction_final, autoSwap_, virtualAddresses] = await Promise.all([
287
+ // Simulate and compute balance diffs + fee.
288
+ // When `capabilities.balanceDiffs` is `false`, skip simulate
289
+ // and compute fee directly from cached metadata.
290
+ (async () => {
291
+ if (!features.simulate)
292
+ return { balanceDiffs: undefined, fee: undefined };
293
+ if (requireBalanceDiffs)
294
+ return simulateAndParseDiffs(client, {
295
+ account: from,
296
+ calls,
297
+ swap,
298
+ feeToken,
299
+ gas: transaction_filled.gas,
300
+ store,
301
+ maxFeePerGas: transaction_filled.maxFeePerGas,
302
+ });
303
+ const fee = await computeFee(client, {
304
+ feeToken,
305
+ gas: transaction_filled.gas,
306
+ store,
307
+ maxFeePerGas: transaction_filled.maxFeePerGas,
308
+ }).catch(() => undefined);
309
+ return { balanceDiffs: undefined, fee };
310
+ })(),
311
+ // Sign as fee payer (if sponsored and not already signed).
312
+ (async () => {
313
+ if (!(sponsored && feePayerOptions && !alreadySigned))
314
+ return transaction_filled;
315
+ return Sponsorship.sign({
316
+ account: feePayerOptions.account,
317
+ sender: from,
318
+ transaction: transaction_filled,
319
+ });
320
+ })(),
321
+ // Resolve autoSwap metadata (when AMM path was taken).
322
+ resolveAutoSwapMetadata(client, { autoSwap, store, swap }),
323
+ // Resolve virtual-address recipients so wallets can show the
324
+ // eventual master address before signing.
325
+ resolveVirtualAddresses(client, { calls }),
326
+ ]);
327
+ const { balanceDiffs, fee } = simulation;
328
+ const sponsor = (() => {
329
+ if (!sponsored)
330
+ return undefined;
331
+ // App-provided fee payer: relay back the sponsor from the upstream response.
332
+ if (externalFeePayerUrl)
333
+ return filled.sponsor;
334
+ if (feePayerOptions)
335
+ return Sponsorship.getSponsor(feePayerOptions);
336
+ return filled.sponsor;
337
+ })();
338
+ return RpcResponse.from({
339
+ result: {
340
+ ...(sponsor ? { sponsor } : {}),
341
+ tx: core_Transaction.toRpc(transaction_final),
342
+ capabilities: {
343
+ balanceDiffs,
344
+ fee,
345
+ sponsored: !!sponsor,
346
+ ...(sponsor ? { sponsor } : {}),
347
+ ...(autoSwap_ ? { autoSwap: autoSwap_ } : {}),
348
+ ...(virtualAddresses ? { virtualAddresses } : {}),
349
+ },
350
+ },
351
+ }, { request });
352
+ }
353
+ catch (error) {
354
+ if (!(error instanceof Error))
355
+ return Utils.rpcErrorJson(request, error);
356
+ if (capabilities['errors'] !== true)
357
+ return Utils.rpcErrorJson(request, error);
358
+ const revert = ExecutionError.parse(error);
359
+ const parameters = request.params?.[0];
360
+ const stub = {
361
+ from: parameters['from'],
362
+ to: parameters['to'] ?? null,
363
+ gas: '0x0',
364
+ nonce: '0x0',
365
+ value: '0x0',
366
+ maxFeePerGas: '0x0',
367
+ maxPriorityFeePerGas: '0x0',
368
+ };
369
+ if (revert?.errorName === 'InsufficientBalance') {
370
+ const args = revert.args;
371
+ const [available, required, token] = args;
372
+ const normalized = Utils.normalizeFillTransactionRequest(parameters);
373
+ // Simulate from zero address for optimistic balance diffs.
374
+ const optimisticCalls = normalized ? extractCalls(normalized) : undefined;
375
+ const [{ balanceDiffs }, virtualAddresses] = optimisticCalls
376
+ ? await Promise.all([
377
+ simulateAndParseDiffs(client, {
378
+ account: zeroAddress,
379
+ calls: optimisticCalls,
380
+ store,
381
+ }),
382
+ resolveVirtualAddresses(client, { calls: optimisticCalls }),
383
+ ])
384
+ : [{ balanceDiffs: undefined }, undefined];
385
+ // Re-key balance diffs from zero address to the real sender.
386
+ const senderDiffs = parameters['from'] && balanceDiffs
387
+ ? { [parameters['from']]: balanceDiffs[zeroAddress] ?? [] }
388
+ : balanceDiffs;
389
+ const metadata = await resolveTokenMetadata(client, { token, store }).catch(() => undefined);
390
+ const deficit = required - available;
391
+ return RpcResponse.from({
392
+ result: {
393
+ tx: stub,
394
+ capabilities: {
395
+ balanceDiffs: senderDiffs,
396
+ error: ExecutionError.serialize(revert),
397
+ requireFunds: metadata
398
+ ? {
399
+ amount: Hex.fromNumber(deficit),
400
+ decimals: metadata.decimals,
401
+ formatted: formatUnits(deficit, metadata.decimals),
402
+ token,
403
+ symbol: metadata.symbol,
404
+ }
405
+ : undefined,
406
+ sponsored: false,
407
+ ...(virtualAddresses ? { virtualAddresses } : {}),
408
+ },
409
+ },
410
+ }, { request });
411
+ }
412
+ const normalized = Utils.normalizeFillTransactionRequest(parameters);
413
+ const virtualAddresses = normalized
414
+ ? await resolveVirtualAddresses(client, { calls: extractCalls(normalized) }).catch(() => undefined)
415
+ : undefined;
416
+ return RpcResponse.from({
417
+ result: {
418
+ tx: stub,
419
+ capabilities: {
420
+ error: ExecutionError.serialize(revert),
421
+ sponsored: false,
422
+ ...(virtualAddresses ? { virtualAddresses } : {}),
423
+ },
424
+ },
425
+ }, { request });
426
+ }
427
+ }
428
+ case 'eth_signRawTransaction':
429
+ case 'eth_sendRawTransaction':
430
+ case 'eth_sendRawTransactionSync': {
431
+ try {
432
+ const serialized = params[0];
433
+ if (multisigOptions &&
434
+ (request.method === 'eth_sendRawTransaction' ||
435
+ request.method === 'eth_sendRawTransactionSync') &&
436
+ typeof serialized === 'string' &&
437
+ Multisig.isMultisigTransaction(serialized)) {
438
+ const result = await Multisig.handleRawTransaction({
439
+ claimTtl: multisigOptions.claimTtl,
440
+ finalize: multisigOptions.finalize,
441
+ getClient,
442
+ method: request.method,
443
+ request: { params: 'params' in request ? request.params : undefined },
444
+ resolveConfig: multisigOptions.resolveConfig,
445
+ sponsor: feePayerOptions
446
+ ? {
447
+ account: feePayerOptions.account,
448
+ feeToken: feePayerOptions.feeToken,
449
+ resolveFeeToken: async (chainId) => (await getTokens(chainId))[0],
450
+ validate: feePayerOptions.validate,
451
+ }
452
+ : undefined,
453
+ store: multisigOptions.store,
454
+ });
455
+ return RpcResponse.from({ result }, { request });
456
+ }
457
+ if (!feePayerOptions) {
458
+ // eth_signRawTransaction is a signing method that only the
459
+ // relay can fulfill — forwarding it to the RPC node returns
460
+ // an opaque "Method not found". Return an actionable error.
461
+ if (request.method === 'eth_signRawTransaction') {
462
+ return RpcResponse.from({
463
+ error: {
464
+ code: -32601,
465
+ message: 'eth_signRawTransaction requires a fee payer to be configured on the relay. ' +
466
+ 'Set the `feePayer` option in `Handler.relay()` to enable transaction sponsorship.',
467
+ },
468
+ }, { request });
469
+ }
470
+ const result = await client.request(request);
471
+ return RpcResponse.from({ result }, { request });
472
+ }
473
+ if (typeof serialized !== 'string' ||
474
+ !Sponsorship.requestsRawSponsorship(serialized)) {
475
+ const result = await client.request(request);
476
+ return RpcResponse.from({ result }, { request });
477
+ }
478
+ const result = await Sponsorship.handleRawTransaction({
479
+ account: feePayerOptions.account,
480
+ feeToken: feePayerOptions.feeToken,
481
+ getFeeToken: async (chainId) => (await getTokens(chainId))[0],
482
+ getClient,
483
+ method: request.method,
484
+ request: { params: 'params' in request ? request.params : undefined },
485
+ validate: feePayerOptions.validate,
486
+ });
487
+ return RpcResponse.from({ result }, { request });
488
+ }
489
+ catch (error) {
490
+ return Utils.rpcErrorJson(request, error);
491
+ }
492
+ }
493
+ case 'eth_getTransactionByHash':
494
+ case 'eth_getTransactionReceipt': {
495
+ try {
496
+ if (multisigOptions) {
497
+ const aliased = await Multisig.handleGetTransaction({
498
+ getClient,
499
+ method: request.method,
500
+ request: { params: 'params' in request ? request.params : undefined },
501
+ store: multisigOptions.store,
502
+ });
503
+ if (aliased)
504
+ return RpcResponse.from({ result: aliased.result }, { request });
505
+ }
506
+ const result = await client.request(request);
507
+ return RpcResponse.from({ result }, { request });
508
+ }
509
+ catch (error) {
510
+ return Utils.rpcErrorJson(request, error);
511
+ }
512
+ }
513
+ default: {
514
+ try {
515
+ const result = await client.request(request);
516
+ return RpcResponse.from({ result }, { request });
517
+ }
518
+ catch (error) {
519
+ return Utils.rpcErrorJson(request, error);
520
+ }
521
+ }
522
+ }
523
+ }
524
+ const router = from(rest);
525
+ async function handlePost(c) {
526
+ const chainId = Utils.resolveChainId(c.req.param('chainId'));
527
+ const body = await c.req.raw.json();
528
+ const isBatch = Array.isArray(body);
529
+ if (!isBatch) {
530
+ const request = RpcRequest.from(body);
531
+ return Response.json(await handleRequest(request, { chainId }));
532
+ }
533
+ const responses = await Promise.all(body.map((item) => handleRequest(RpcRequest.from(item), {
534
+ chainId,
535
+ })));
536
+ return Response.json(responses);
537
+ }
538
+ router.post(path, handlePost);
539
+ router.post(`${path === '/' ? '' : path}/:chainId`, handlePost);
540
+ return router;
541
+ }
542
+ // TODO: cleanup
543
+ async function fill(client, options) {
544
+ const { autoSwap, feeToken, store, transaction: request } = options;
545
+ // Skip re-formatting if already in RPC format (e.g. from viem's fillTransaction).
546
+ const format = (value) => value['type'] === '0x76' ? value : Utils.formatFillTransactionRequest(client, value);
547
+ // Re-fill the transaction with prepended swap calls so the user can
548
+ // mint the missing `insufficientToken` (typically the fee token) from
549
+ // a token they already hold. Returns null if no source token is
550
+ // available or autoSwap is disabled.
551
+ async function fillWithSwap(insufficientToken, deficit) {
552
+ if (!autoSwap)
553
+ return null;
554
+ const sourceToken = feeToken && feeToken.toLowerCase() !== insufficientToken.toLowerCase()
555
+ ? feeToken
556
+ : await options.resolveFeeToken?.(insufficientToken);
557
+ if (!sourceToken || sourceToken.toLowerCase() === insufficientToken.toLowerCase())
558
+ return null;
559
+ const maxAmountIn = deficit + (deficit * BigInt(Math.round(autoSwap.slippage * 1000))) / 1000n;
560
+ const originalCalls = request['calls'] ?? [];
561
+ const swapCalls = buildSwapCalls(client, sourceToken, insufficientToken, deficit, maxAmountIn);
562
+ const result = await client.request({
563
+ method: 'eth_fillTransaction',
564
+ params: [
565
+ format({
566
+ ...request,
567
+ calls: [...swapCalls, ...originalCalls],
568
+ }),
569
+ ],
570
+ });
571
+ const sponsor = result['capabilities']?.sponsor;
572
+ const mergedTx = mergeCallsFromRequest(result.tx, {
573
+ ...request,
574
+ calls: [...swapCalls, ...originalCalls],
575
+ });
576
+ return {
577
+ transaction: Utils.normalizeTempoTransaction(mergedTx),
578
+ sponsor,
579
+ swap: {
580
+ calls: swapCalls,
581
+ tokenIn: sourceToken,
582
+ tokenOut: insufficientToken,
583
+ amountOut: deficit,
584
+ maxAmountIn,
585
+ },
586
+ };
587
+ }
588
+ try {
589
+ const formatted = format(request);
590
+ const result = await client.request({
591
+ method: 'eth_fillTransaction',
592
+ params: [formatted],
593
+ });
594
+ // FIXME: node estimates gas with secp256k1 dummy sig + null feePayerSignature.
595
+ // Actual tx has larger keychain/webAuthn sigs + real fee payer sig, costing
596
+ // more intrinsic gas. Mirror the bump from viem's tempo chainConfig.
597
+ // Skip if another relay already bumped (indicated by feePayerSignature).
598
+ // @ts-expect-error
599
+ if (result.tx.gas && request.feePayer && !result.tx.feePayerSignature)
600
+ result.tx.gas = Hex.fromNumber(BigInt(result.tx.gas) + 20000n);
601
+ const upstreamCapabilities = result.capabilities;
602
+ const sponsor = upstreamCapabilities?.['sponsor'];
603
+ // External fee-payer relays surface chain reverts (e.g. InsufficientBalance,
604
+ // InsufficientAllowance) inside `capabilities.error` with a stub `tx`
605
+ // instead of throwing. Re-throw all upstream errors so the outer handler
606
+ // either recovers via autoSwap (InsufficientBalance) or propagates the
607
+ // error to the caller. Without this, the wallet would otherwise sign the
608
+ // zero-stub tx with its own fee payer and broadcast garbage.
609
+ const upstreamError = upstreamCapabilities?.['error'];
610
+ if (upstreamError) {
611
+ const synthetic = new Error(upstreamError.message ?? upstreamError.errorName ?? 'UpstreamRevert');
612
+ synthetic.name = 'UpstreamRevertError';
613
+ synthetic.data = upstreamError.data;
614
+ throw synthetic;
615
+ }
616
+ // Reconstruct a `swap` shape from upstream's `capabilities.autoSwap` so the
617
+ // wallet relay's outer code can re-resolve autoSwap metadata locally —
618
+ // otherwise upstream-driven swaps are silently dropped from the response.
619
+ const swap = extractSwapFromCapabilities(upstreamCapabilities?.['autoSwap']);
620
+ // The chain's `eth_fillTransaction` doesn't echo back `calls`, so merge
621
+ // them in from the original request before normalizing — otherwise the
622
+ // typed envelope built for sponsorship signing throws CallsEmptyError.
623
+ const mergedTx = mergeCallsFromRequest(result.tx, request);
624
+ // The node's `eth_fillTransaction` may pick a fee token the user
625
+ // doesn't yet hold (e.g. one this transaction will mint via a
626
+ // swap). Tempo's gas check runs before the calls execute, so the
627
+ // tx would revert at broadcast with `have 0 want N`. Validate the
628
+ // sender's pre-tx balance against the computed gas cost and, if
629
+ // short, autoSwap a token they do hold into the fee token.
630
+ if (autoSwap && !swap) {
631
+ const fromAddress = request['from'];
632
+ const resolvedFeeToken = (mergedTx['feeToken'] ?? feeToken);
633
+ const gas = mergedTx['gas'] ? BigInt(mergedTx['gas']) : 0n;
634
+ const maxFeePerGas = mergedTx['maxFeePerGas']
635
+ ? BigInt(mergedTx['maxFeePerGas'])
636
+ : 0n;
637
+ if (fromAddress && resolvedFeeToken && gas > 0n && maxFeePerGas > 0n) {
638
+ const [balance, metadata] = await Promise.all([
639
+ Actions.token
640
+ .getBalance(client, { account: fromAddress, token: resolvedFeeToken })
641
+ .then((balance) => balance.amount)
642
+ .catch(() => 0n),
643
+ resolveTokenMetadata(client, { token: resolvedFeeToken, store }).catch(() => undefined),
644
+ ]);
645
+ if (metadata) {
646
+ const requiredFee = (gas * maxFeePerGas) / 10n ** BigInt(Math.max(0, 18 - metadata.decimals));
647
+ if (balance < requiredFee) {
648
+ // Best-effort: if the swap itself can't be filled (e.g. the
649
+ // source token also has insufficient balance), fall through
650
+ // and return the original tx with the resolved feeToken
651
+ // rather than failing the whole request.
652
+ const swapResult = await fillWithSwap(resolvedFeeToken, requiredFee - balance).catch(() => null);
653
+ if (swapResult)
654
+ return swapResult;
655
+ }
656
+ }
657
+ }
658
+ }
659
+ return {
660
+ transaction: Utils.normalizeTempoTransaction(mergedTx),
661
+ sponsor,
662
+ ...(swap ? { swap } : {}),
663
+ };
664
+ }
665
+ catch (error) {
666
+ if (!(error instanceof Error))
667
+ throw error;
668
+ if (!autoSwap)
669
+ throw error;
670
+ const revert = ExecutionError.parse(error);
671
+ if (revert?.errorName !== 'InsufficientBalance')
672
+ throw error;
673
+ const [available, required, token] = revert.args;
674
+ if (typeof available === 'undefined' || typeof required === 'undefined' || !token)
675
+ throw error;
676
+ const swapResult = await fillWithSwap(token, required - available);
677
+ if (!swapResult)
678
+ throw error;
679
+ return swapResult;
680
+ }
681
+ }
682
+ async function resolveFeeToken(client, options) {
683
+ const { feeToken, account, store, tokens } = options;
684
+ if (feeToken)
685
+ return feeToken;
686
+ if (!account)
687
+ return undefined;
688
+ // Cache the user's preferred fee token for `userTokenCacheTtl` seconds.
689
+ // The on-chain preference rarely changes; a short TTL avoids the
690
+ // ~150-250ms `userTokens` multicall on every unsponsored fill while
691
+ // still picking up updates within a minute. The cached value is just a
692
+ // hint — if the user's balance is 0 we fall back to the highest-balance
693
+ // token from the configured list.
694
+ const getUserToken = () => Actions.fee.getUserToken(client, { account }).catch(() => null);
695
+ const userTokenPromise = store
696
+ ? Store.memoize(async () => {
697
+ const result = await getUserToken();
698
+ return result ? { address: result.address } : null;
699
+ }, {
700
+ key: `fee.userToken:${client.chain?.id ?? 0}:${account.toLowerCase()}`,
701
+ store,
702
+ ttl: (options.userTokenCacheTtl ?? 60) * 1000,
703
+ })
704
+ : getUserToken();
705
+ const [userToken, balances] = await Promise.all([
706
+ userTokenPromise,
707
+ tokens
708
+ ? Promise.all(tokens.map(async (token) => ({
709
+ address: token,
710
+ balance: await Actions.token
711
+ .getBalance(client, { account, token })
712
+ .then((balance) => balance.amount)
713
+ .catch(() => 0n),
714
+ })))
715
+ : [],
716
+ ]);
717
+ // If on-chain preference is set and user has balance, use it.
718
+ if (userToken) {
719
+ const match = balances.find((b) => b.address.toLowerCase() === userToken.address.toLowerCase() && b.balance > 0n);
720
+ if (match)
721
+ return userToken.address;
722
+ // Token list may not include the preference — check on-chain directly.
723
+ if (!match) {
724
+ try {
725
+ const { amount: balance } = await Actions.token.getBalance(client, {
726
+ account,
727
+ token: userToken.address,
728
+ });
729
+ if (balance > 0n)
730
+ return userToken.address;
731
+ }
732
+ catch { }
733
+ }
734
+ }
735
+ // Pick the token with the highest balance.
736
+ let best;
737
+ for (const asset of balances) {
738
+ if (asset.balance <= 0n)
739
+ continue;
740
+ if (!best || asset.balance > best.balance)
741
+ best = asset;
742
+ }
743
+ if (best)
744
+ return best.address;
745
+ return undefined;
746
+ }
747
+ /**
748
+ * Extracts unique TIP20 token addresses (Tempo `0x20c0…` prefix) that the
749
+ * transaction's calls target. Used as fallback fee-token candidates so a
750
+ * user transferring a token they hold can pay gas in that token even when
751
+ * the configured fee-token list defaults to one they don't hold.
752
+ */
753
+ function callTargetTokens(transaction) {
754
+ const calls = transaction['calls'];
755
+ if (!calls)
756
+ return [];
757
+ const out = [];
758
+ const seen = new Set();
759
+ for (const c of calls) {
760
+ if (!c.to)
761
+ continue;
762
+ const lower = c.to.toLowerCase();
763
+ if (!lower.startsWith('0x20c0'))
764
+ continue;
765
+ if (seen.has(lower))
766
+ continue;
767
+ seen.add(lower);
768
+ out.push(c.to);
769
+ }
770
+ return out;
771
+ }
772
+ async function resolveVirtualAddresses(client, options) {
773
+ const targets = getVirtualAddressTargets(options.calls);
774
+ if (targets.length === 0)
775
+ return undefined;
776
+ const masters = new Map();
777
+ for (const address of targets) {
778
+ const { masterId } = VirtualAddress.parse(address);
779
+ const lower = masterId.toLowerCase();
780
+ const entry = masters.get(lower) ?? { addresses: [], masterId };
781
+ entry.addresses.push(address);
782
+ masters.set(lower, entry);
783
+ }
784
+ const entries = await Promise.all([...masters.values()].map(async ({ addresses, masterId }) => {
785
+ const master = await Actions.virtualAddress.getMasterAddress(client, { masterId });
786
+ return addresses.map((address) => [address, master]);
787
+ }));
788
+ return Object.fromEntries(entries.flat());
789
+ }
790
+ function getVirtualAddressTargets(calls) {
791
+ const targets = new Set();
792
+ for (const call of calls) {
793
+ for (const address of [call.to, decodeTransferRecipient(call.data)]) {
794
+ if (!address || !isAddress(address) || !VirtualAddress.isVirtual(address))
795
+ continue;
796
+ targets.add(address.toLowerCase());
797
+ }
798
+ }
799
+ return [...targets];
800
+ }
801
+ function decodeTransferRecipient(data) {
802
+ if (!data)
803
+ return undefined;
804
+ const selector = data.slice(0, 10).toLowerCase();
805
+ if (!transferSelectors.has(selector))
806
+ return undefined;
807
+ try {
808
+ const { args, functionName } = decodeFunctionData({
809
+ abi: Abis.tip20,
810
+ data: data,
811
+ });
812
+ if ((functionName === 'transfer' || functionName === 'transferWithMemo') &&
813
+ typeof args[0] === 'string')
814
+ return args[0];
815
+ if ((functionName === 'transferFrom' || functionName === 'transferFromWithMemo') &&
816
+ typeof args[1] === 'string')
817
+ return args[1];
818
+ }
819
+ catch { }
820
+ return undefined;
821
+ }
822
+ const transferSelectors = new Set(['0xa9059cbb', '0x95777d59', '0x23b872dd', '0x929c2539']);
823
+ // TODO: cleanup/remove
824
+ function extractCalls(transaction) {
825
+ const calls = transaction['calls'];
826
+ if (calls && calls.length > 0)
827
+ return calls.map((c) => ({
828
+ ...(c.to ? { to: c.to } : {}),
829
+ ...(c.data ? { data: c.data } : {}),
830
+ ...(c.value ? { value: c.value } : {}),
831
+ }));
832
+ return [
833
+ {
834
+ ...(transaction['to'] ? { to: transaction['to'] } : {}),
835
+ ...(transaction['data'] ? { data: transaction['data'] } : {}),
836
+ ...(transaction['value'] ? { value: transaction['value'] } : {}),
837
+ },
838
+ ];
839
+ }
840
+ // TODO: cleanup/remove
841
+ async function simulate(client, options) {
842
+ const { account, calls } = options;
843
+ try {
844
+ return await Actions.simulate.simulateCalls(client, {
845
+ ...(account ? { account } : {}),
846
+ calls: calls,
847
+ traceTransfers: true,
848
+ });
849
+ }
850
+ catch (error) {
851
+ // TODO: Remove fallback once all nodes support tempo_simulateV1.
852
+ // Fall back to viem's simulateCalls (eth_simulateV1) if the Tempo
853
+ // method (tempo_simulateV1) is not supported.
854
+ const code = error.code ??
855
+ error.cause?.code;
856
+ if (code !== -32601)
857
+ throw error;
858
+ const { results } = await simulateCalls(client, {
859
+ ...(account ? { account } : {}),
860
+ calls: calls,
861
+ });
862
+ return { results, tokenMetadata: undefined };
863
+ }
864
+ }
865
+ async function simulateAndParseDiffs(client, options) {
866
+ const { account, calls, swap, feeToken, gas, store, maxFeePerGas } = options;
867
+ try {
868
+ const { results, tokenMetadata } = await simulate(client, {
869
+ account: account === zeroAddress ? undefined : account,
870
+ calls,
871
+ });
872
+ // Collect all logs across all call results.
873
+ const logs = [];
874
+ for (const result of results)
875
+ if (result.logs)
876
+ logs.push(...result.logs);
877
+ // Build per-token balance diffs relative to the sender.
878
+ const balanceDiffs = account
879
+ ? await buildBalanceDiffs(client, {
880
+ account,
881
+ store,
882
+ logs,
883
+ swap,
884
+ tokenMetadata: tokenMetadata,
885
+ })
886
+ : {};
887
+ // Compute fee breakdown.
888
+ const fee = await computeFee(client, {
889
+ feeToken,
890
+ gas,
891
+ store,
892
+ maxFeePerGas,
893
+ tokenMetadata: tokenMetadata,
894
+ }).catch(() => undefined);
895
+ return { balanceDiffs, fee };
896
+ }
897
+ catch {
898
+ // Simulation failures should not block the fill response —
899
+ // return empty diffs with fee computed from transaction fields.
900
+ const fee = await computeFee(client, { feeToken, gas, store, maxFeePerGas });
901
+ return { balanceDiffs: undefined, fee };
902
+ }
903
+ }
904
+ async function buildBalanceDiffs(client, options) {
905
+ const { account, store, logs, swap, tokenMetadata } = options;
906
+ const accountLower = account.toLowerCase();
907
+ const dexLower = Addresses.stablecoinDex.toLowerCase();
908
+ const swapTokenIn = swap?.tokenIn.toLowerCase();
909
+ const swapTokenOut = swap?.tokenOut.toLowerCase();
910
+ const transferLogs = parseEventLogs({
911
+ abi: [AbiEvent.fromAbi(Abis.tip20, 'Transfer')],
912
+ eventName: 'Transfer',
913
+ logs,
914
+ });
915
+ const approvalLogs = parseEventLogs({
916
+ abi: [AbiEvent.fromAbi(Abis.tip20, 'Approval')],
917
+ eventName: 'Approval',
918
+ logs,
919
+ });
920
+ // Track net movement per token: incoming vs outgoing.
921
+ const tokenMap = new Map();
922
+ // Track total transferred per (token, spender) so we can suppress covered approvals.
923
+ const transferredBySpender = new Map();
924
+ for (const log of transferLogs) {
925
+ const token = log.address.toLowerCase();
926
+ const fromLower = log.args.from.toLowerCase();
927
+ const toLower = log.args.to.toLowerCase();
928
+ // Skip swap-related transfers (reported in capabilities.autoSwap instead).
929
+ if (swap) {
930
+ if (token === swapTokenIn && fromLower === accountLower && toLower === dexLower)
931
+ continue;
932
+ if (token === swapTokenOut && fromLower === dexLower && toLower === accountLower)
933
+ continue;
934
+ }
935
+ const entry = tokenMap.get(token) ?? {
936
+ incoming: 0n,
937
+ outgoing: 0n,
938
+ recipients: new Set(),
939
+ token: log.address,
940
+ };
941
+ if (fromLower === accountLower) {
942
+ entry.outgoing += log.args.amount;
943
+ entry.recipients.add(log.args.to);
944
+ const key = `${token}:${toLower}`;
945
+ transferredBySpender.set(key, (transferredBySpender.get(key) ?? 0n) + log.args.amount);
946
+ }
947
+ if (toLower === accountLower)
948
+ entry.incoming += log.args.amount;
949
+ tokenMap.set(token, entry);
950
+ }
951
+ // Treat approvals as outgoing unless the spender already transferred >= approval amount.
952
+ for (const log of approvalLogs) {
953
+ if (log.args.owner.toLowerCase() !== accountLower)
954
+ continue;
955
+ const token = log.address.toLowerCase();
956
+ // Skip swap-related approvals (reported in capabilities.autoSwap instead).
957
+ if (swap && token === swapTokenIn && log.args.spender.toLowerCase() === dexLower)
958
+ continue;
959
+ const spenderKey = `${token}:${log.args.spender.toLowerCase()}`;
960
+ const transferred = transferredBySpender.get(spenderKey) ?? 0n;
961
+ if (log.args.amount <= transferred)
962
+ continue;
963
+ const entry = tokenMap.get(token) ?? {
964
+ incoming: 0n,
965
+ outgoing: 0n,
966
+ recipients: new Set(),
967
+ token: log.address,
968
+ };
969
+ entry.outgoing += log.args.amount - transferred;
970
+ entry.recipients.add(log.args.spender);
971
+ tokenMap.set(token, entry);
972
+ }
973
+ // Collect unique tokens that need decimals.
974
+ const entries = [...tokenMap.values()].filter((e) => {
975
+ const net = e.outgoing > e.incoming ? e.outgoing - e.incoming : e.incoming - e.outgoing;
976
+ return net > 0n;
977
+ });
978
+ if (entries.length === 0)
979
+ return {};
980
+ // Resolve metadata for all tokens in parallel (simulation metadata first, RPC fallback).
981
+ const metadataMap = new Map();
982
+ await Promise.all(entries.map(async (entry) => {
983
+ try {
984
+ const metadata = await resolveTokenMetadata(client, {
985
+ token: entry.token,
986
+ tokenMetadata,
987
+ store,
988
+ });
989
+ metadataMap.set(entry.token.toLowerCase(), metadata);
990
+ }
991
+ catch { }
992
+ }));
993
+ // Build the diff array for this account.
994
+ const diffs = [];
995
+ for (const entry of entries) {
996
+ const net = entry.outgoing > entry.incoming
997
+ ? entry.outgoing - entry.incoming
998
+ : entry.incoming - entry.outgoing;
999
+ const direction = entry.outgoing > entry.incoming ? 'outgoing' : 'incoming';
1000
+ const meta = metadataMap.get(entry.token.toLowerCase());
1001
+ const decimals = meta?.decimals ?? 0;
1002
+ diffs.push({
1003
+ address: entry.token,
1004
+ decimals,
1005
+ direction,
1006
+ formatted: formatUnits(net, decimals),
1007
+ name: meta?.name ?? '',
1008
+ symbol: meta?.symbol ?? '',
1009
+ recipients: [...entry.recipients],
1010
+ value: Hex.fromNumber(net),
1011
+ });
1012
+ }
1013
+ return { [account]: diffs };
1014
+ }
1015
+ async function resolveTokenMetadata(client, options) {
1016
+ const { token, tokenMetadata, store } = options;
1017
+ const meta = tokenMetadata?.[token] ?? tokenMetadata?.[token.toLowerCase()];
1018
+ // TIP-20 metadata (decimals/symbol/name) is immutable per token, so cache
1019
+ // long-term. Skips the multicall RPC on cache hits.
1020
+ const fetcher = () => Actions.token.getMetadata(client, { token });
1021
+ const fallback = store
1022
+ ? await Store.memoize(fetcher, {
1023
+ key: `tokenMetadata:${client.chain?.id ?? 0}:${token.toLowerCase()}`,
1024
+ store,
1025
+ ttl: 24 * 60 * 60 * 1000,
1026
+ })
1027
+ : await fetcher();
1028
+ return {
1029
+ decimals: fallback.decimals ?? 6,
1030
+ symbol: meta?.symbol || fallback.symbol,
1031
+ name: meta?.name || fallback.name,
1032
+ };
1033
+ }
1034
+ async function resolveAutoSwapMetadata(client, options) {
1035
+ const { autoSwap, store, swap } = options;
1036
+ if (!autoSwap || !swap)
1037
+ return undefined;
1038
+ const [inMeta, outMeta] = await Promise.all([
1039
+ resolveTokenMetadata(client, { token: swap.tokenIn, store }).catch(() => undefined),
1040
+ resolveTokenMetadata(client, { token: swap.tokenOut, store }).catch(() => undefined),
1041
+ ]);
1042
+ if (!inMeta || !outMeta)
1043
+ return undefined;
1044
+ return {
1045
+ calls: swap.calls.map((c) => ({ to: c.to, data: c.data })),
1046
+ slippage: autoSwap.slippage,
1047
+ maxIn: {
1048
+ token: swap.tokenIn,
1049
+ value: Hex.fromNumber(swap.maxAmountIn),
1050
+ formatted: formatUnits(swap.maxAmountIn, inMeta.decimals),
1051
+ decimals: inMeta.decimals,
1052
+ symbol: inMeta.symbol,
1053
+ name: inMeta.name,
1054
+ },
1055
+ minOut: {
1056
+ token: swap.tokenOut,
1057
+ value: Hex.fromNumber(swap.amountOut),
1058
+ formatted: formatUnits(swap.amountOut, outMeta.decimals),
1059
+ decimals: outMeta.decimals,
1060
+ symbol: outMeta.symbol,
1061
+ name: outMeta.name,
1062
+ },
1063
+ };
1064
+ }
1065
+ async function computeFee(client, options) {
1066
+ const { feeToken, gas, store, maxFeePerGas, tokenMetadata } = options;
1067
+ if (!feeToken || !gas || !maxFeePerGas)
1068
+ return undefined;
1069
+ try {
1070
+ const metadata = await resolveTokenMetadata(client, { token: feeToken, tokenMetadata, store });
1071
+ const raw = gas * maxFeePerGas;
1072
+ const amount = raw / 10n ** BigInt(18 - metadata.decimals);
1073
+ return {
1074
+ amount: Hex.fromNumber(amount),
1075
+ decimals: metadata.decimals,
1076
+ formatted: formatUnits(amount, metadata.decimals),
1077
+ symbol: metadata.symbol,
1078
+ };
1079
+ }
1080
+ catch {
1081
+ return undefined;
1082
+ }
1083
+ }
1084
+ function buildSwapCalls(client, sourceToken, targetToken, deficit, maxAmountIn) {
1085
+ const approve = Actions.token.approve.call(client, {
1086
+ token: sourceToken,
1087
+ spender: Addresses.stablecoinDex,
1088
+ amount: maxAmountIn,
1089
+ });
1090
+ const buy = Actions.dex.buy.call({
1091
+ tokenIn: sourceToken,
1092
+ tokenOut: targetToken,
1093
+ amountOut: deficit,
1094
+ maxAmountIn,
1095
+ });
1096
+ return [
1097
+ { to: approve.to, data: approve.data, value: 0n },
1098
+ { to: buy.to, data: buy.data, value: 0n },
1099
+ ];
1100
+ }
1101
+ /**
1102
+ * Merges the original fill request into the result tx. The chain's
1103
+ * `eth_fillTransaction` returns only the "filled" gas/nonce/fee fields and
1104
+ * omits envelope inputs like `calls`, `chainId`, `validBefore`, `nonceKey`,
1105
+ * `keyData`, `keyType`, `feePayer`. Without these the typed Tempo envelope
1106
+ * built for sponsorship signing throws `CallsEmptyError` or
1107
+ * `Cannot convert undefined to a BigInt` when serializing.
1108
+ *
1109
+ * Result fields take precedence (they are the chain's authoritative filled
1110
+ * values); request fields fill in everything else. Calls are normalized
1111
+ * separately so legacy `to`/`data`/`value` requests are also supported.
1112
+ */
1113
+ function mergeCallsFromRequest(resultTx, request) {
1114
+ const merged = { ...request, ...resultTx };
1115
+ const resultCalls = resultTx['calls'];
1116
+ if (Array.isArray(resultCalls) && resultCalls.length > 0)
1117
+ return merged;
1118
+ const reqCalls = request['calls'];
1119
+ if (Array.isArray(reqCalls) && reqCalls.length > 0) {
1120
+ merged['calls'] = reqCalls;
1121
+ return merged;
1122
+ }
1123
+ const { to, data, value } = request;
1124
+ if (typeof to === 'undefined' && typeof data === 'undefined' && typeof value === 'undefined')
1125
+ return merged;
1126
+ merged['calls'] = [
1127
+ {
1128
+ ...(typeof to !== 'undefined' ? { to } : {}),
1129
+ ...(typeof data !== 'undefined' ? { data } : {}),
1130
+ ...(typeof value !== 'undefined' ? { value } : {}),
1131
+ },
1132
+ ];
1133
+ return merged;
1134
+ }
1135
+ /** Normalizes external fee-payer relay URLs before forwarding fill payloads. */
1136
+ function normalizeExternalFeePayerUrl(value, options = {}) {
1137
+ let url;
1138
+ try {
1139
+ url = new URL(value);
1140
+ }
1141
+ catch {
1142
+ throw new RpcResponse.InvalidParamsError({ message: 'Invalid fee payer URL.' });
1143
+ }
1144
+ const allowUnsafe = options.allowUnsafe === true;
1145
+ if (url.protocol !== 'http:' && url.protocol !== 'https:')
1146
+ throw new RpcResponse.InvalidParamsError({ message: 'Invalid fee payer URL protocol.' });
1147
+ if (!allowUnsafe && url.protocol !== 'https:')
1148
+ throw new RpcResponse.InvalidParamsError({ message: 'Invalid fee payer URL protocol.' });
1149
+ if (!allowUnsafe && isUnsafeHostname(url.hostname))
1150
+ throw new RpcResponse.InvalidParamsError({ message: 'Invalid fee payer URL host.' });
1151
+ url.username = '';
1152
+ url.password = '';
1153
+ url.hash = '';
1154
+ return url.href;
1155
+ }
1156
+ function isUnsafeHostname(hostname) {
1157
+ const host = normalizeHostname(hostname);
1158
+ if (host === 'localhost' || host.endsWith('.localhost'))
1159
+ return true;
1160
+ const ipv4 = parseIpv4(host);
1161
+ if (ipv4)
1162
+ return isUnsafeIpv4(ipv4);
1163
+ if (!host.includes(':'))
1164
+ return false;
1165
+ const ipv6 = parseIpv6(host);
1166
+ if (!ipv6)
1167
+ return true;
1168
+ return isUnsafeIpv6(ipv6);
1169
+ }
1170
+ function normalizeHostname(hostname) {
1171
+ return hostname
1172
+ .toLowerCase()
1173
+ .replace(/^\[|\]$/g, '')
1174
+ .split('%')[0];
1175
+ }
1176
+ function parseIpv4(value) {
1177
+ const parts = value.split('.');
1178
+ if (parts.length !== 4 || parts.some((part) => !/^\d+$/.test(part)))
1179
+ return undefined;
1180
+ const numbers = parts.map((part) => Number(part));
1181
+ if (numbers.some((part) => !Number.isInteger(part) || part < 0 || part > 255))
1182
+ return undefined;
1183
+ return numbers;
1184
+ }
1185
+ function isUnsafeIpv4(parts) {
1186
+ const [a, b] = parts;
1187
+ if (a === 0 || a === 10 || a === 127)
1188
+ return true;
1189
+ if (a === 100 && b >= 64 && b <= 127)
1190
+ return true;
1191
+ if (a === 169 && b === 254)
1192
+ return true;
1193
+ if (a === 172 && b >= 16 && b <= 31)
1194
+ return true;
1195
+ if (a === 192 && b === 168)
1196
+ return true;
1197
+ if (a >= 224)
1198
+ return true;
1199
+ return false;
1200
+ }
1201
+ function parseIpv6(value) {
1202
+ const halves = value.split('::');
1203
+ if (halves.length > 2)
1204
+ return undefined;
1205
+ const left = halves[0] ? halves[0].split(':') : [];
1206
+ const right = halves[1] ? halves[1].split(':') : [];
1207
+ const missing = halves.length === 2 ? 8 - left.length - right.length : 0;
1208
+ if (missing < 0)
1209
+ return undefined;
1210
+ const parts = [...left, ...Array(missing).fill('0'), ...right];
1211
+ if (parts.length !== 8)
1212
+ return undefined;
1213
+ const numbers = parts.map((part) => /^[\da-f]{1,4}$/i.test(part) ? Number.parseInt(part, 16) : NaN);
1214
+ if (numbers.some((part) => !Number.isInteger(part) || part < 0 || part > 0xffff))
1215
+ return undefined;
1216
+ return numbers;
1217
+ }
1218
+ function isUnsafeIpv6(parts) {
1219
+ const [a] = parts;
1220
+ if (parts.every((part) => part === 0))
1221
+ return true;
1222
+ if (parts.slice(0, 7).every((part) => part === 0) && parts[7] === 1)
1223
+ return true;
1224
+ if ((a & 0xfe00) === 0xfc00)
1225
+ return true;
1226
+ if ((a & 0xffc0) === 0xfe80)
1227
+ return true;
1228
+ if ((a & 0xffc0) === 0xfec0)
1229
+ return true;
1230
+ if ((a & 0xff00) === 0xff00)
1231
+ return true;
1232
+ const ipv4 = ipv4FromIpv6(parts);
1233
+ if (ipv4)
1234
+ return isUnsafeIpv4(ipv4);
1235
+ return false;
1236
+ }
1237
+ function ipv4FromIpv6(parts) {
1238
+ const mapped = parts.slice(0, 5).every((part) => part === 0) && parts[5] === 0xffff;
1239
+ const compatible = parts.slice(0, 6).every((part) => part === 0);
1240
+ if (!mapped && !compatible)
1241
+ return undefined;
1242
+ return [parts[6] >> 8, parts[6] & 0xff, parts[7] >> 8, parts[7] & 0xff];
1243
+ }
1244
+ /**
1245
+ * Reconstructs a `swap` shape (matching the inner autoSwap branch's return
1246
+ * value) from an upstream relay's `capabilities.autoSwap`. Used so a wallet
1247
+ * relay forwarding to an external feePayer URL can re-emit autoSwap metadata
1248
+ * locally without losing track of the upstream's swap.
1249
+ */
1250
+ function extractSwapFromCapabilities(autoSwap) {
1251
+ if (!autoSwap || typeof autoSwap !== 'object')
1252
+ return undefined;
1253
+ const a = autoSwap;
1254
+ if (!a.calls || !a.maxIn?.token || !a.maxIn.value || !a.minOut?.token || !a.minOut.value)
1255
+ return undefined;
1256
+ return {
1257
+ calls: a.calls,
1258
+ tokenIn: a.maxIn.token,
1259
+ tokenOut: a.minOut.token,
1260
+ amountOut: BigInt(a.minOut.value),
1261
+ maxAmountIn: BigInt(a.maxIn.value),
1262
+ };
1263
+ }
1264
+ //# sourceMappingURL=relay.js.map