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