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,2035 @@
1
+ import type { RpcRequest } from 'ox'
2
+ import { SignatureEnvelope, TxEnvelopeTempo } from 'ox/tempo'
3
+ import { parseUnits, type Address, type BaseError } from 'viem'
4
+ import { fillTransaction, sendTransactionSync } from 'viem/actions'
5
+ import { Actions, Addresses, Capabilities, Tick, Transaction, VirtualAddress } from 'viem/tempo'
6
+ import { tempo, tempoModerato } from 'viem/tempo/chains'
7
+
8
+ import * as Relay from '../../test/Relay.js'
9
+ import * as Runtime from '../../test/runtime.js'
10
+
11
+ type Server = Relay.Server
12
+
13
+ const userAccount = Relay.accounts[9]!
14
+ const feePayerAccount = Relay.accounts[0]!
15
+ const recipient = Relay.accounts[7]!
16
+
17
+ /**
18
+ * Tokens the relay handler probes for fee-token resolution. The default
19
+ * `resolveTokens` fetches `tokenlist.tempo.xyz`, which doesn't know about
20
+ * the localnet chain — so tests inject this list explicitly.
21
+ */
22
+ const localnetTokens = [
23
+ {
24
+ address: '0x20c0000000000000000000000000000000000000',
25
+ decimals: 6,
26
+ name: 'pathUSD',
27
+ symbol: 'pathUSD',
28
+ },
29
+ {
30
+ address: '0x20c0000000000000000000000000000000000001',
31
+ decimals: 6,
32
+ name: 'alphaUSD',
33
+ symbol: 'alphaUSD',
34
+ },
35
+ {
36
+ address: '0x20c0000000000000000000000000000000000002',
37
+ decimals: 6,
38
+ name: 'betaUSD',
39
+ symbol: 'betaUSD',
40
+ },
41
+ {
42
+ address: '0x20c0000000000000000000000000000000000003',
43
+ decimals: 6,
44
+ name: 'thetaUSD',
45
+ symbol: 'thetaUSD',
46
+ },
47
+ ] as const
48
+
49
+ /** Case-insensitive lookup into balanceDiffs keyed by address. */
50
+ function findDiffs(
51
+ balanceDiffs: Capabilities.FillTransactionCapabilities['balanceDiffs'],
52
+ address: string,
53
+ ) {
54
+ return Object.entries(balanceDiffs ?? {}).find(
55
+ ([addr]) => addr.toLowerCase() === address.toLowerCase(),
56
+ )?.[1]
57
+ }
58
+
59
+ /** Extracts relay virtual-address metadata while viem's public type catches up. */
60
+ function virtualAddresses(capabilities: Capabilities.FillTransactionCapabilities | undefined) {
61
+ return (
62
+ capabilities as
63
+ | (Capabilities.FillTransactionCapabilities & {
64
+ virtualAddresses?: Record<Address, Address | null> | undefined
65
+ })
66
+ | undefined
67
+ )?.virtualAddresses
68
+ }
69
+
70
+ /** Client used only to build typed calldata (`Actions.token.*.call`). */
71
+ const caller = Relay.getClient()
72
+
73
+ /** A simple transfer call for tests that just need a valid transaction. */
74
+ const transferCall = () =>
75
+ Actions.token.transfer.call(caller, {
76
+ token: Relay.addresses.alphaUsd,
77
+ to: recipient.address,
78
+ amount: 1n,
79
+ })
80
+
81
+ beforeAll(async () => {
82
+ // Faucet-fund the accounts that pay fees or create fresh tokens. The faucet is
83
+ // permissionless, so no genesis privileges are required. Left unfunded on
84
+ // purpose: accounts[5]/[10] (balance-sensitive fee-resolution and
85
+ // insufficient-balance tests) and accounts[7] (recipient only).
86
+ await Promise.all(
87
+ [0, 2, 3, 4, 6, 8, 9].map((index) =>
88
+ Actions.faucet.fundSync(Relay.getClient(), {
89
+ account: Relay.accounts[index]!,
90
+ timeout: 60_000,
91
+ }),
92
+ ),
93
+ )
94
+ // userAccount prefers alphaUsd (a faucet-funded genesis token) as its fee token.
95
+ await Actions.fee.setUserToken(Relay.getClient(), {
96
+ account: userAccount,
97
+ token: Relay.addresses.alphaUsd,
98
+ })
99
+ })
100
+
101
+ describe.skipIf(Runtime.get().mode !== 'localnet')('default', () => {
102
+ let client: ReturnType<typeof Relay.getClient>
103
+ let server: Server
104
+
105
+ beforeAll(async () => {
106
+ server = await Relay.createServer((await Relay.relay({})).listener)
107
+ client = Relay.getClient({ url: server.url })
108
+ })
109
+
110
+ afterAll(() => {
111
+ server.close()
112
+ })
113
+
114
+ test('default: returns filled transaction with capabilities', async () => {
115
+ const { transaction } = await fillTransaction(client, {
116
+ account: userAccount.address,
117
+ calls: [transferCall()],
118
+ })
119
+
120
+ expect(transaction.gas).toBeDefined()
121
+ expect(transaction.nonce).toBeDefined()
122
+ })
123
+
124
+ test('behavior: proxies other methods to RPC node', async () => {
125
+ const chainId = await client.request({ method: 'eth_chainId' })
126
+ expect(Number(chainId)).toMatchInlineSnapshot(`${Relay.chain.id}`)
127
+ })
128
+
129
+ test('behavior: surfaces upstream RPC errors as JSON-RPC errors', async () => {
130
+ // grantRoles reverts with Unauthorized when caller is not an admin
131
+ // (eth_call defaults `from` to the zero address). We expect the relay to
132
+ // forward the revert as a structured JSON-RPC error response (HTTP 200
133
+ // with `error.code`/`error.data`), not a 500.
134
+ const call = Actions.token.grantRoles.call(caller, {
135
+ token: Relay.addresses.alphaUsd,
136
+ role: 'issuer',
137
+ to: recipient.address,
138
+ })
139
+
140
+ const response = await fetch(server.url, {
141
+ method: 'POST',
142
+ headers: { 'Content-Type': 'application/json' },
143
+ body: JSON.stringify({
144
+ jsonrpc: '2.0',
145
+ id: 1,
146
+ method: 'eth_call',
147
+ params: [{ to: call.to, data: call.data }, 'latest'],
148
+ }),
149
+ })
150
+
151
+ expect(response.status).toBe(200)
152
+ const body = (await response.json()) as {
153
+ id: number
154
+ jsonrpc: string
155
+ error: { code: number; message: string; data?: string }
156
+ }
157
+ // Drop `message` from the snapshot — it embeds the upstream RPC URL/port
158
+ // and viem version, which are nondeterministic.
159
+ const { message: _message, ...errorRest } = body.error
160
+ expect({ ...body, error: errorRest }).toMatchInlineSnapshot(`
161
+ {
162
+ "error": {
163
+ "code": 3,
164
+ "data": "0x82b42900",
165
+ },
166
+ "id": 1,
167
+ "jsonrpc": "2.0",
168
+ }
169
+ `)
170
+ })
171
+
172
+ test('behavior: returns actionable error for eth_signRawTransaction without feePayer', async () => {
173
+ const response = await fetch(server.url, {
174
+ method: 'POST',
175
+ headers: { 'Content-Type': 'application/json' },
176
+ body: JSON.stringify({
177
+ jsonrpc: '2.0',
178
+ id: 1,
179
+ method: 'eth_signRawTransaction',
180
+ params: ['0x00'],
181
+ }),
182
+ })
183
+
184
+ expect(response.status).toBe(200)
185
+ const body = (await response.json()) as {
186
+ id: number
187
+ jsonrpc: string
188
+ error: { code: number; message: string }
189
+ }
190
+ expect(body.error.code).toBe(-32601)
191
+ expect(body.error.message).toContain('fee payer')
192
+ expect(body.error.message).toContain('Handler.relay()')
193
+ })
194
+
195
+ test('behavior: handles JSON-RPC batch requests', async () => {
196
+ const response = await fetch(server.url, {
197
+ method: 'POST',
198
+ headers: { 'Content-Type': 'application/json' },
199
+ body: JSON.stringify([
200
+ { jsonrpc: '2.0', id: 1, method: 'eth_chainId', params: [] },
201
+ { jsonrpc: '2.0', id: 2, method: 'eth_chainId', params: [] },
202
+ ]),
203
+ })
204
+
205
+ expect(response.status).toBe(200)
206
+ const body = (await response.json()) as { id: number; result: string }[]
207
+ expect(Array.isArray(body)).toBe(true)
208
+ expect(body).toHaveLength(2)
209
+ expect(body[0]!.id).toBe(1)
210
+ expect(body[1]!.id).toBe(2)
211
+ expect(Number(body[0]!.result)).toBe(Relay.chain.id)
212
+ expect(Number(body[1]!.result)).toBe(Relay.chain.id)
213
+ })
214
+ })
215
+
216
+ describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: with feePayer', () => {
217
+ let server: Server
218
+ let client: ReturnType<typeof Relay.getClient>
219
+ let requests: RpcRequest.RpcRequest[] = []
220
+
221
+ beforeAll(async () => {
222
+ server = await Relay.createServer(
223
+ (
224
+ await Relay.relay({
225
+ feePayer: {
226
+ account: feePayerAccount,
227
+ name: 'Test Sponsor',
228
+ url: 'https://test.com',
229
+ },
230
+ onRequest: async (request) => {
231
+ requests.push(request)
232
+ },
233
+ })
234
+ ).listener,
235
+ )
236
+ client = Relay.getClient({ url: server.url })
237
+ })
238
+
239
+ afterAll(() => {
240
+ server.close()
241
+ })
242
+
243
+ afterEach(() => {
244
+ requests = []
245
+ })
246
+
247
+ test('default: returns sponsored tx with feePayerSignature', async () => {
248
+ const { transaction } = await fillTransaction(client, {
249
+ account: userAccount.address,
250
+ calls: [transferCall()],
251
+ })
252
+
253
+ expect(transaction.feePayerSignature).toBeDefined()
254
+ expect(requests.map(({ method }) => method)).toMatchInlineSnapshot(`
255
+ [
256
+ "eth_fillTransaction",
257
+ ]
258
+ `)
259
+ })
260
+
261
+ test('behavior: returns sponsor capabilities', async () => {
262
+ const result = await fillTransaction(client, {
263
+ account: userAccount.address,
264
+ calls: [transferCall()],
265
+ })
266
+ const meta = result.capabilities
267
+
268
+ expect(meta?.['sponsored']).toBe(true)
269
+ expect(meta?.['sponsor']).toMatchInlineSnapshot(`
270
+ {
271
+ "address": "${feePayerAccount.address}",
272
+ "name": "Test Sponsor",
273
+ "url": "https://test.com",
274
+ }
275
+ `)
276
+ })
277
+
278
+ test('behavior: sponsored tx can be signed and broadcast', async () => {
279
+ const { transaction } = await fillTransaction(client, {
280
+ account: userAccount.address,
281
+ calls: [transferCall()],
282
+ })
283
+ const signed = await userAccount.signTransaction(transaction as never)
284
+ const receipt = (await Relay.getClient().request({
285
+ method: 'eth_sendRawTransactionSync' as never,
286
+ params: [signed],
287
+ })) as { feePayer?: string | undefined }
288
+
289
+ expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
290
+ })
291
+
292
+ test('behavior: missing from returns error capability when errors capability is enabled', async () => {
293
+ const result = await fillTransaction(client, {
294
+ calls: [transferCall()],
295
+ capabilities: { errors: true },
296
+ })
297
+ expect(result.capabilities).toMatchInlineSnapshot(`
298
+ {
299
+ "error": {
300
+ "errorName": "unknown",
301
+ "message": "unknown account",
302
+ },
303
+ "sponsored": false,
304
+ }
305
+ `)
306
+ })
307
+ })
308
+
309
+ describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: with feePayer.feeToken', () => {
310
+ let server: Server
311
+ let client: ReturnType<typeof Relay.getClient>
312
+
313
+ const sponsorFeeToken = '0x20c0000000000000000000000000000000000000' as const // pathUSD
314
+
315
+ beforeAll(async () => {
316
+ server = await Relay.createServer(
317
+ (
318
+ await Relay.relay({
319
+ feePayer: {
320
+ account: feePayerAccount,
321
+ feeToken: sponsorFeeToken,
322
+ },
323
+ tokens: localnetTokens,
324
+ })
325
+ ).listener,
326
+ )
327
+ client = Relay.getClient({ url: server.url })
328
+ })
329
+
330
+ afterAll(() => {
331
+ server.close()
332
+ })
333
+
334
+ test('default: sponsor.feeToken is used when request omits feeToken', async () => {
335
+ const { transaction } = await fillTransaction(client, {
336
+ account: userAccount.address,
337
+ calls: [transferCall()],
338
+ })
339
+ expect(transaction.feeToken?.toLowerCase()).toBe(sponsorFeeToken)
340
+ expect(transaction.feePayerSignature).toBeDefined()
341
+ })
342
+
343
+ test('behavior: sponsor.feeToken overrides request feeToken on sponsored fills', async () => {
344
+ const { transaction } = await fillTransaction(client, {
345
+ account: userAccount.address,
346
+ calls: [transferCall()],
347
+ feeToken: Relay.addresses.alphaUsd as Address,
348
+ })
349
+ expect(transaction.feeToken?.toLowerCase()).toBe(sponsorFeeToken)
350
+ expect(transaction.feePayerSignature).toBeDefined()
351
+ })
352
+
353
+ test('behavior: request feeToken wins when sponsorship is opted out via feePayer:false', async () => {
354
+ const { transaction } = await fillTransaction(client, {
355
+ account: userAccount.address,
356
+ calls: [transferCall()],
357
+ feePayer: false as never,
358
+ feeToken: Relay.addresses.alphaUsd as Address,
359
+ })
360
+ expect(transaction.feeToken?.toLowerCase()).toBe(Relay.addresses.alphaUsd.toLowerCase())
361
+ expect(transaction.feePayerSignature).toBeUndefined()
362
+ })
363
+
364
+ test('behavior: broadcast tx receipt records the sponsor.feeToken', async () => {
365
+ // Fill via relay (where override kicks in), then sign + broadcast manually.
366
+ // viem's `sendTransactionSync` with a hydrated account does local prep and
367
+ // would skip the relay's `eth_fillTransaction`, bypassing the override.
368
+ const { transaction } = await fillTransaction(client, {
369
+ account: userAccount.address,
370
+ calls: [transferCall()],
371
+ feeToken: Relay.addresses.alphaUsd as Address,
372
+ })
373
+ expect(transaction.feeToken?.toLowerCase()).toBe(sponsorFeeToken)
374
+
375
+ const signed = await userAccount.signTransaction(transaction as never)
376
+ const receipt = (await Relay.getClient().request({
377
+ method: 'eth_sendRawTransactionSync' as never,
378
+ params: [signed],
379
+ })) as { feePayer?: string | undefined; feeToken?: string | undefined }
380
+
381
+ expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
382
+ expect(receipt.feeToken?.toLowerCase()).toBe(sponsorFeeToken)
383
+ })
384
+
385
+ test('behavior: raw sponsor signing restores sponsor.feeToken when envelope omits feeToken', async () => {
386
+ const rpc = Relay.getClient({ account: userAccount })
387
+ const { transaction } = await fillTransaction(rpc, {
388
+ account: userAccount.address,
389
+ calls: [transferCall()],
390
+ feePayer: true as never,
391
+ })
392
+ const partial = await userAccount.signTransaction({ ...transaction, feePayer: true } as never)
393
+ expect([null, undefined]).toContain(
394
+ Transaction.deserialize(partial as `0x76${string}`).feeToken,
395
+ )
396
+
397
+ const response = await fetch(server.url, {
398
+ method: 'POST',
399
+ headers: { 'Content-Type': 'application/json' },
400
+ body: JSON.stringify({
401
+ jsonrpc: '2.0',
402
+ id: 1,
403
+ method: 'eth_signRawTransaction',
404
+ params: [partial],
405
+ }),
406
+ })
407
+ const body = (await response.json()) as { result: `0x76${string}` }
408
+ const feeToken = Transaction.deserialize(body.result).feeToken as Address | undefined
409
+
410
+ expect(feeToken?.toLowerCase()).toBe(sponsorFeeToken)
411
+ })
412
+
413
+ test('behavior: raw sponsor signing restores token-list default before validation', async () => {
414
+ let feeToken_validated: Address | undefined
415
+ const customServer = await Relay.createServer(
416
+ (
417
+ await Relay.relay({
418
+ feePayer: {
419
+ account: feePayerAccount,
420
+ validate: (request) => {
421
+ feeToken_validated = request.feeToken as Address | undefined
422
+ return true
423
+ },
424
+ },
425
+ tokens: localnetTokens,
426
+ })
427
+ ).listener,
428
+ )
429
+ const rpc = Relay.getClient({ account: userAccount })
430
+ const { transaction } = await fillTransaction(rpc, {
431
+ account: userAccount.address,
432
+ calls: [transferCall()],
433
+ feePayer: true as never,
434
+ })
435
+ const partial = await userAccount.signTransaction({ ...transaction, feePayer: true } as never)
436
+
437
+ try {
438
+ const response = await fetch(customServer.url, {
439
+ method: 'POST',
440
+ headers: { 'Content-Type': 'application/json' },
441
+ body: JSON.stringify({
442
+ jsonrpc: '2.0',
443
+ id: 1,
444
+ method: 'eth_signRawTransaction',
445
+ params: [partial],
446
+ }),
447
+ })
448
+ const body = (await response.json()) as { result: `0x76${string}` }
449
+ const feeToken = Transaction.deserialize(body.result).feeToken as Address | undefined
450
+
451
+ expect(feeToken_validated?.toLowerCase()).toBe(sponsorFeeToken)
452
+ expect(feeToken?.toLowerCase()).toBe(sponsorFeeToken)
453
+ } finally {
454
+ customServer.close()
455
+ }
456
+ })
457
+ })
458
+
459
+ describe.skipIf(Runtime.get().mode !== 'localnet')(
460
+ 'behavior: with app-provided feePayer URL',
461
+ () => {
462
+ let appServer: Server
463
+ let walletServer: Server
464
+ let client: ReturnType<typeof Relay.getClient>
465
+
466
+ beforeAll(async () => {
467
+ // App relay: has a fee payer account and signs transactions.
468
+ appServer = await Relay.createServer(
469
+ (
470
+ await Relay.relay({
471
+ feePayer: {
472
+ account: feePayerAccount,
473
+ name: 'App Sponsor',
474
+ url: 'https://app.example.com',
475
+ },
476
+ })
477
+ ).listener,
478
+ )
479
+
480
+ // Wallet relay: no fee payer configured — proxies to app relay.
481
+ walletServer = await Relay.createServer(
482
+ (
483
+ await Relay.relay({
484
+ internal_allowUnsafeUrls: true,
485
+ })
486
+ ).listener,
487
+ )
488
+
489
+ client = Relay.getClient({ url: walletServer.url })
490
+ })
491
+
492
+ afterAll(() => {
493
+ appServer.close()
494
+ walletServer.close()
495
+ })
496
+
497
+ test('default: proxies fill to app relay and returns sponsored tx', async () => {
498
+ const { transaction } = await fillTransaction(client, {
499
+ account: userAccount.address,
500
+ calls: [transferCall()],
501
+ feePayer: appServer.url as never,
502
+ })
503
+
504
+ expect(transaction.feePayerSignature).toBeDefined()
505
+ expect(transaction.gas).toBeDefined()
506
+ })
507
+
508
+ test('behavior: relays sponsor metadata from app relay', async () => {
509
+ const result = await fillTransaction(client, {
510
+ account: userAccount.address,
511
+ calls: [transferCall()],
512
+ feePayer: appServer.url as never,
513
+ })
514
+
515
+ expect(result.capabilities?.['sponsored']).toBe(true)
516
+ expect(result.capabilities?.['sponsor']).toMatchInlineSnapshot(`
517
+ {
518
+ "address": "${feePayerAccount.address}",
519
+ "name": "App Sponsor",
520
+ "url": "https://app.example.com",
521
+ }
522
+ `)
523
+ })
524
+
525
+ test('behavior: sponsored tx from app relay can be signed and broadcast', async () => {
526
+ const { transaction } = await fillTransaction(client, {
527
+ account: userAccount.address,
528
+ calls: [transferCall()],
529
+ feePayer: appServer.url as never,
530
+ })
531
+ const signed = await userAccount.signTransaction(transaction as never)
532
+ const receipt = (await Relay.getClient().request({
533
+ method: 'eth_sendRawTransactionSync' as never,
534
+ params: [signed],
535
+ })) as { feePayer?: string | undefined }
536
+
537
+ expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
538
+ })
539
+ },
540
+ )
541
+
542
+ describe.skipIf(Runtime.get().mode !== 'localnet')(
543
+ 'behavior: with app-provided feePayer URL + autoSwap',
544
+ () => {
545
+ let appServer: Server
546
+ let walletServer: Server
547
+ let client: ReturnType<typeof Relay.getClient>
548
+
549
+ beforeAll(async () => {
550
+ // App relay sponsors fees AND has `features: 'all'` so it can recover
551
+ // from InsufficientBalance via autoSwap.
552
+ appServer = await Relay.createServer(
553
+ (
554
+ await Relay.relay({
555
+ features: 'all',
556
+ feePayer: {
557
+ account: feePayerAccount,
558
+ name: 'App Sponsor',
559
+ url: 'https://app.example.com',
560
+ },
561
+ })
562
+ ).listener,
563
+ )
564
+
565
+ // Wallet relay forwards to the app relay; also has features:'all' so its
566
+ // own fill() can detect upstream `capabilities.error` as InsufficientBalance.
567
+ walletServer = await Relay.createServer(
568
+ (
569
+ await Relay.relay({
570
+ features: 'all',
571
+ internal_allowUnsafeUrls: true,
572
+ })
573
+ ).listener,
574
+ )
575
+
576
+ client = Relay.getClient({ url: walletServer.url })
577
+ })
578
+
579
+ afterAll(() => {
580
+ appServer.close()
581
+ walletServer.close()
582
+ })
583
+
584
+ test('behavior: autoSwap recovers when external feePayer surfaces InsufficientBalance', async () => {
585
+ const sender = Relay.accounts[6]!
586
+
587
+ // Token pair + DEX liquidity. Use alphaUsd as the quote token so the
588
+ // relay can swap alphaUsd → base to cover the deficit.
589
+ const rpc = Relay.getClient({ account: Relay.accounts[0]! })
590
+ const { token: base } = await Actions.token.createSync(rpc, {
591
+ name: 'External Swap Base',
592
+ symbol: 'EXTBASE',
593
+ currency: 'USD',
594
+ quoteToken: Relay.addresses.alphaUsd,
595
+ })
596
+ await sendTransactionSync(rpc, {
597
+ calls: [
598
+ Actions.token.grantRoles.call(caller, {
599
+ token: base,
600
+ role: 'issuer',
601
+ to: rpc.account!.address,
602
+ }),
603
+ Actions.token.mint.call(caller, {
604
+ token: base,
605
+ to: rpc.account!.address,
606
+ amount: parseUnits('10000', 6),
607
+ }),
608
+ Actions.token.approve.call(caller, {
609
+ token: base,
610
+ spender: Addresses.stablecoinDex,
611
+ amount: parseUnits('10000', 6),
612
+ }),
613
+ Actions.token.approve.call(caller, {
614
+ token: Relay.addresses.alphaUsd,
615
+ spender: Addresses.stablecoinDex,
616
+ amount: parseUnits('10000', 6),
617
+ }),
618
+ ],
619
+ })
620
+ await Actions.dex.createPairSync(rpc, { base })
621
+ await Actions.dex.placeSync(rpc, {
622
+ token: base,
623
+ amount: parseUnits('500', 6),
624
+ type: 'sell',
625
+ tick: Tick.fromPrice('1.001'),
626
+ })
627
+
628
+ // Sender has faucet alphaUsd (fee + swap source) but NO base tokens.
629
+ await Actions.fee.setUserToken(Relay.getClient({ account: sender }), {
630
+ token: Relay.addresses.alphaUsd,
631
+ })
632
+
633
+ // Sender attempts to transfer base via the wallet relay, which forwards
634
+ // to the app relay. The app relay returns 200 with capabilities.error =
635
+ // InsufficientBalance and a stub tx; the wallet relay must convert that
636
+ // into a synthetic throw so its own fill() autoSwap branch can recover.
637
+ const transferAmount = parseUnits('5', 6)
638
+ const result = await fillTransaction(client, {
639
+ account: sender.address,
640
+ ...Actions.token.transfer.call(caller, {
641
+ token: base,
642
+ to: Relay.accounts[7]!.address,
643
+ amount: transferAmount,
644
+ }),
645
+ feePayer: appServer.url as never,
646
+ })
647
+
648
+ const { transaction, capabilities } = result
649
+
650
+ // Tx is filled with the swap calls prepended (approve + buy + transfer).
651
+ expect(transaction.calls).toHaveLength(3)
652
+ expect(transaction.feePayerSignature).toBeDefined()
653
+
654
+ // autoSwap metadata is surfaced.
655
+ expect(capabilities?.['autoSwap']?.slippage).toBe(0.05)
656
+ expect(capabilities?.['autoSwap']?.maxIn.symbol).toBe('AlphaUSD')
657
+ expect(capabilities?.['autoSwap']?.minOut.symbol).toBe('EXTBASE')
658
+ expect(capabilities?.['autoSwap']?.minOut.formatted).toBe('5')
659
+ })
660
+ },
661
+ )
662
+
663
+ describe.skipIf(Runtime.get().mode !== 'localnet')(
664
+ 'behavior: app-provided feePayer URL bypasses wallet validate',
665
+ () => {
666
+ let appServer: Server
667
+ let walletServer: Server
668
+ let client: ReturnType<typeof Relay.getClient>
669
+
670
+ beforeAll(async () => {
671
+ // App relay is the authoritative sponsor — it has its own fee payer
672
+ // account and signs sponsored transactions.
673
+ appServer = await Relay.createServer(
674
+ (
675
+ await Relay.relay({
676
+ feePayer: {
677
+ account: feePayerAccount,
678
+ name: 'App Sponsor',
679
+ url: 'https://app.example.com',
680
+ },
681
+ })
682
+ ).listener,
683
+ )
684
+
685
+ // Wallet relay has its own fee payer with a `validate` that ALWAYS
686
+ // rejects. This guards the wallet's own fee payer; it must NOT gate
687
+ // sponsorship when the dapp supplies its own external feePayer URL.
688
+ walletServer = await Relay.createServer(
689
+ (
690
+ await Relay.relay({
691
+ features: 'all',
692
+ internal_allowUnsafeUrls: true,
693
+ feePayer: {
694
+ account: feePayerAccount,
695
+ name: 'Wallet Sponsor',
696
+ validate: () => false,
697
+ },
698
+ })
699
+ ).listener,
700
+ )
701
+
702
+ client = Relay.getClient({ url: walletServer.url })
703
+ })
704
+
705
+ afterAll(() => {
706
+ appServer.close()
707
+ walletServer.close()
708
+ })
709
+
710
+ test('behavior: external feePayer URL is sponsored even when wallet validate rejects', async () => {
711
+ const result = await fillTransaction(client, {
712
+ account: userAccount.address,
713
+ calls: [transferCall()],
714
+ feePayer: appServer.url as never,
715
+ })
716
+
717
+ expect(result.transaction.feePayerSignature).toBeDefined()
718
+ expect(result.transaction.maxFeePerGas).toBeDefined()
719
+ expect(result.transaction.maxFeePerGas).not.toBe(0n)
720
+ expect(result.capabilities?.['sponsored']).toBe(true)
721
+ expect(result.capabilities?.['sponsor']?.name).toBe('App Sponsor')
722
+ })
723
+ },
724
+ )
725
+
726
+ describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: chainId path parameter', () => {
727
+ let server: Server
728
+ let client: ReturnType<typeof Relay.getClient>
729
+
730
+ beforeAll(async () => {
731
+ server = await Relay.createServer((await Relay.relay({})).listener)
732
+ client = Relay.getClient({ url: `${server.url}/${Relay.chain.id}` })
733
+ })
734
+
735
+ afterAll(() => {
736
+ server.close()
737
+ })
738
+
739
+ test('default: proxies RPC methods via /:chainId path', async () => {
740
+ const chainId = await client.request({ method: 'eth_chainId' })
741
+ expect(Number(chainId)).toMatchInlineSnapshot(`${Relay.chain.id}`)
742
+ })
743
+
744
+ test('behavior: fills transaction via /:chainId path', async () => {
745
+ const { transaction } = await fillTransaction(client, {
746
+ account: userAccount.address,
747
+ calls: [transferCall()],
748
+ })
749
+
750
+ expect(transaction.gas).toBeDefined()
751
+ expect(transaction.nonce).toBeDefined()
752
+ })
753
+
754
+ test('behavior: handles batch requests via /:chainId path', async () => {
755
+ const response = await fetch(`${server.url}/${Relay.chain.id}`, {
756
+ method: 'POST',
757
+ headers: { 'Content-Type': 'application/json' },
758
+ body: JSON.stringify([
759
+ { jsonrpc: '2.0', id: 1, method: 'eth_chainId', params: [] },
760
+ { jsonrpc: '2.0', id: 2, method: 'eth_chainId', params: [] },
761
+ ]),
762
+ })
763
+
764
+ expect(response.status).toBe(200)
765
+ const body = (await response.json()) as { id: number; result: string }[]
766
+ expect(body).toHaveLength(2)
767
+ expect(Number(body[0]!.result)).toBe(Relay.chain.id)
768
+ expect(Number(body[1]!.result)).toBe(Relay.chain.id)
769
+ })
770
+ })
771
+
772
+ describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: capabilities', () => {
773
+ let server: Server
774
+ let client: ReturnType<typeof Relay.getClient>
775
+
776
+ beforeAll(async () => {
777
+ server = await Relay.createServer(
778
+ (
779
+ await Relay.relay({
780
+ features: 'all',
781
+ })
782
+ ).listener,
783
+ )
784
+ client = Relay.getClient({ url: server.url })
785
+ })
786
+
787
+ afterAll(() => {
788
+ server.close()
789
+ })
790
+
791
+ test('default: returns fee and sponsored info', async () => {
792
+ const result = await fillTransaction(client, {
793
+ account: userAccount.address,
794
+ calls: [transferCall()],
795
+ })
796
+ const meta = result.capabilities
797
+
798
+ expect(meta?.['fee']).toBeDefined()
799
+ expect(meta?.['fee']?.decimals).toBe(6)
800
+ expect(meta?.['fee']?.symbol).toBe('AlphaUSD')
801
+ expect(meta?.['sponsored']).toBe(false)
802
+ })
803
+
804
+ test('behavior: token transfer produces balance diffs', async () => {
805
+ const sender = Relay.accounts[6]!
806
+ const recipient = Relay.accounts[7]!
807
+ const token = Relay.addresses.alphaUsd
808
+
809
+ // Sender is faucet-funded with alphaUsd (enough for transfer + fee).
810
+ // Set fee token so relay doesn't need pathUSD balance.
811
+ await Actions.fee.setUserToken(Relay.getClient(), { account: sender, token })
812
+
813
+ const { data, to: callTo } = Actions.token.transfer.call(caller, {
814
+ token,
815
+ to: recipient.address,
816
+ amount: 100n,
817
+ })
818
+ const result = await fillTransaction(client, {
819
+ account: sender.address,
820
+ to: callTo,
821
+ data,
822
+ })
823
+
824
+ const meta = result.capabilities
825
+ const senderDiffs = findDiffs(meta?.['balanceDiffs'], sender.address)!
826
+ const tokenDiff = senderDiffs.find((d) => d.address.toLowerCase() === token.toLowerCase())!
827
+ expect(tokenDiff.decimals).toBe(6)
828
+ expect(tokenDiff.direction).toBe('outgoing')
829
+ expect(tokenDiff.formatted).toBe('0.0001')
830
+ expect(tokenDiff.symbol).toBe('AlphaUSD')
831
+ expect(tokenDiff.value).toBe('0x64')
832
+ })
833
+
834
+ test('behavior: resolves direct virtual-address targets', async () => {
835
+ const virtualAddress = VirtualAddress.from({
836
+ masterId: '0xffffffff',
837
+ userTag: '0x000000000001',
838
+ })
839
+
840
+ const result = await fillTransaction(client, {
841
+ account: userAccount.address,
842
+ to: virtualAddress,
843
+ })
844
+
845
+ expect(virtualAddresses(result.capabilities)).toMatchInlineSnapshot(`
846
+ {
847
+ "0xfffffffffdfdfdfdfdfdfdfdfdfd000000000001": null,
848
+ }
849
+ `)
850
+ })
851
+
852
+ test('behavior: resolves TIP-20 memo transfer virtual-address recipients', async () => {
853
+ const virtualAddress = VirtualAddress.from({
854
+ masterId: '0xfffffffe',
855
+ userTag: '0x000000000002',
856
+ })
857
+
858
+ const result = await fillTransaction(client, {
859
+ account: userAccount.address,
860
+ calls: [
861
+ Actions.token.transfer.call(caller, {
862
+ amount: 1n,
863
+ memo: '0x01',
864
+ to: virtualAddress,
865
+ token: Relay.addresses.alphaUsd,
866
+ }),
867
+ ],
868
+ capabilities: { errors: true },
869
+ })
870
+
871
+ expect(virtualAddresses(result.capabilities)).toMatchInlineSnapshot(`
872
+ {
873
+ "0xfffffffefdfdfdfdfdfdfdfdfdfd000000000002": null,
874
+ }
875
+ `)
876
+ })
877
+
878
+ test('behavior: approve + dex swap + transfer produces balance diffs', async () => {
879
+ const sender = Relay.accounts[8]!
880
+ const recipient = Relay.accounts[7]!
881
+
882
+ // Set up token pair + DEX liquidity.
883
+ const rpc = Relay.getClient({ account: Relay.accounts[0]! })
884
+ const { token: quote } = await Actions.token.createSync(rpc, {
885
+ name: 'Test Quote',
886
+ symbol: 'TQUOTE',
887
+ currency: 'USD',
888
+ })
889
+ const { token: base } = await Actions.token.createSync(rpc, {
890
+ name: 'Test Base',
891
+ symbol: 'TBASE',
892
+ currency: 'USD',
893
+ quoteToken: quote,
894
+ })
895
+ await sendTransactionSync(rpc, {
896
+ calls: [
897
+ Actions.token.grantRoles.call(caller, {
898
+ token: base,
899
+ role: 'issuer',
900
+ to: rpc.account!.address,
901
+ }),
902
+ Actions.token.grantRoles.call(caller, {
903
+ token: quote,
904
+ role: 'issuer',
905
+ to: rpc.account!.address,
906
+ }),
907
+ Actions.token.mint.call(caller, {
908
+ token: base,
909
+ to: rpc.account!.address,
910
+ amount: parseUnits('10000', 6),
911
+ }),
912
+ Actions.token.mint.call(caller, {
913
+ token: quote,
914
+ to: rpc.account!.address,
915
+ amount: parseUnits('10000', 6),
916
+ }),
917
+ Actions.token.approve.call(caller, {
918
+ token: base,
919
+ spender: Addresses.stablecoinDex,
920
+ amount: parseUnits('10000', 6),
921
+ }),
922
+ Actions.token.approve.call(caller, {
923
+ token: quote,
924
+ spender: Addresses.stablecoinDex,
925
+ amount: parseUnits('10000', 6),
926
+ }),
927
+ ],
928
+ })
929
+ await Actions.dex.createPairSync(rpc, { base })
930
+ await Actions.dex.placeSync(rpc, {
931
+ token: base,
932
+ amount: parseUnits('500', 6),
933
+ type: 'sell',
934
+ tick: Tick.fromPrice('1.001'),
935
+ })
936
+
937
+ // Fund sender with quote tokens; alphaUsd fees come from the faucet.
938
+ await Actions.token.mintSync(rpc, {
939
+ token: quote,
940
+ amount: parseUnits('1000', 6),
941
+ to: sender.address,
942
+ })
943
+ await Actions.fee.setUserToken(Relay.getClient({ account: sender }), {
944
+ token: Relay.addresses.alphaUsd,
945
+ })
946
+
947
+ const buyAmount = parseUnits('10', 6)
948
+ const result = await fillTransaction(client, {
949
+ account: sender.address,
950
+ calls: [
951
+ Actions.token.approve.call(caller, {
952
+ token: quote,
953
+ spender: Addresses.stablecoinDex,
954
+ amount: parseUnits('100', 6),
955
+ }),
956
+ Actions.dex.buy.call({
957
+ tokenIn: quote,
958
+ tokenOut: base,
959
+ amountOut: buyAmount,
960
+ maxAmountIn: parseUnits('100', 6),
961
+ }),
962
+ Actions.token.transfer.call(caller, {
963
+ token: base,
964
+ to: recipient.address,
965
+ amount: buyAmount,
966
+ }),
967
+ ],
968
+ })
969
+ const meta = result.capabilities
970
+
971
+ const diffs = findDiffs(meta?.['balanceDiffs'], sender.address)!
972
+ expect(diffs).toHaveLength(1)
973
+
974
+ const quoteDiff = diffs[0]!
975
+ expect(quoteDiff.address.toLowerCase()).toBe(quote.toLowerCase())
976
+ expect(quoteDiff.direction).toBe('outgoing')
977
+ expect(quoteDiff.symbol).toBe('TQUOTE')
978
+ expect(quoteDiff.name).toBe('Test Quote')
979
+ expect(quoteDiff.decimals).toBe(6)
980
+ // No base diff — bought and immediately transferred out (net zero).
981
+ expect(diffs.find((d) => d.address.toLowerCase() === base.toLowerCase())).toBeUndefined()
982
+ })
983
+
984
+ test('behavior: approval covered by transfer is suppressed', async () => {
985
+ const sender = Relay.accounts[6]!
986
+ const recipient = Relay.accounts[7]!
987
+ const token = Relay.addresses.alphaUsd
988
+
989
+ // approve(100) + transfer(100) to same spender → approval fully covered.
990
+ const result = await fillTransaction(client, {
991
+ account: sender.address,
992
+ calls: [
993
+ Actions.token.approve.call(caller, {
994
+ token,
995
+ spender: recipient.address,
996
+ amount: 100n,
997
+ }),
998
+ Actions.token.transfer.call(caller, {
999
+ token,
1000
+ to: recipient.address,
1001
+ amount: 100n,
1002
+ }),
1003
+ ],
1004
+ })
1005
+ const meta = result.capabilities
1006
+
1007
+ const diffs = findDiffs(meta?.['balanceDiffs'], sender.address)!
1008
+ const tokenDiff = diffs.find((d) => d.address.toLowerCase() === token.toLowerCase())!
1009
+ // Only the transfer shows — approval is fully covered.
1010
+ expect(tokenDiff.value).toBe('0x64')
1011
+ expect(tokenDiff.direction).toBe('outgoing')
1012
+ })
1013
+
1014
+ test('behavior: uncovered approval shows as outgoing', async () => {
1015
+ const sender = Relay.accounts[6]!
1016
+ const spender = Relay.accounts[7]!
1017
+ const token = Relay.addresses.alphaUsd
1018
+
1019
+ // approve(200) + transfer(50) to same spender → 150 uncovered approval.
1020
+ const result = await fillTransaction(client, {
1021
+ account: sender.address,
1022
+ calls: [
1023
+ Actions.token.approve.call(caller, {
1024
+ token,
1025
+ spender: spender.address,
1026
+ amount: 200n,
1027
+ }),
1028
+ Actions.token.transfer.call(caller, {
1029
+ token,
1030
+ to: spender.address,
1031
+ amount: 50n,
1032
+ }),
1033
+ ],
1034
+ })
1035
+ const meta = result.capabilities
1036
+
1037
+ const diffs = findDiffs(meta?.['balanceDiffs'], sender.address)!
1038
+ const tokenDiff = diffs.find((d) => d.address.toLowerCase() === token.toLowerCase())!
1039
+ // transfer(50) + uncovered approval(150) = 200 outgoing.
1040
+ expect(tokenDiff.value).toBe('0xc8')
1041
+ expect(tokenDiff.direction).toBe('outgoing')
1042
+ })
1043
+ })
1044
+
1045
+ describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: AMM resolution', () => {
1046
+ let server: Server
1047
+ let client: ReturnType<typeof Relay.getClient>
1048
+
1049
+ beforeAll(async () => {
1050
+ server = await Relay.createServer(
1051
+ (
1052
+ await Relay.relay({
1053
+ features: 'all',
1054
+ })
1055
+ ).listener,
1056
+ )
1057
+ client = Relay.getClient({ url: server.url })
1058
+ })
1059
+
1060
+ afterAll(() => {
1061
+ server.close()
1062
+ })
1063
+
1064
+ test('behavior: prepends swap calls on InsufficientBalance', async () => {
1065
+ const sender = Relay.accounts[4]!
1066
+
1067
+ // Set up token pair + DEX liquidity.
1068
+ // Use alphaUsd as the quote token so the relay can swap alphaUsd → base.
1069
+ const rpc = Relay.getClient({ account: Relay.accounts[0]! })
1070
+ const { token: base } = await Actions.token.createSync(rpc, {
1071
+ name: 'Swap Base',
1072
+ symbol: 'SWBASE',
1073
+ currency: 'USD',
1074
+ quoteToken: Relay.addresses.alphaUsd,
1075
+ })
1076
+ await sendTransactionSync(rpc, {
1077
+ calls: [
1078
+ Actions.token.grantRoles.call(caller, {
1079
+ token: base,
1080
+ role: 'issuer',
1081
+ to: rpc.account!.address,
1082
+ }),
1083
+ Actions.token.mint.call(caller, {
1084
+ token: base,
1085
+ to: rpc.account!.address,
1086
+ amount: parseUnits('10000', 6),
1087
+ }),
1088
+ Actions.token.approve.call(caller, {
1089
+ token: base,
1090
+ spender: Addresses.stablecoinDex,
1091
+ amount: parseUnits('10000', 6),
1092
+ }),
1093
+ Actions.token.approve.call(caller, {
1094
+ token: Relay.addresses.alphaUsd,
1095
+ spender: Addresses.stablecoinDex,
1096
+ amount: parseUnits('10000', 6),
1097
+ }),
1098
+ ],
1099
+ })
1100
+ await Actions.dex.createPairSync(rpc, { base })
1101
+ await Actions.dex.placeSync(rpc, {
1102
+ token: base,
1103
+ amount: parseUnits('500', 6),
1104
+ type: 'sell',
1105
+ tick: Tick.fromPrice('1.001'),
1106
+ })
1107
+
1108
+ // Sender has faucet alphaUsd (fee token) but NO base tokens.
1109
+ await Actions.fee.setUserToken(Relay.getClient({ account: sender }), {
1110
+ token: Relay.addresses.alphaUsd,
1111
+ })
1112
+
1113
+ // Sender tries to transfer base tokens they don't have.
1114
+ // Relay should detect InsufficientBalance, swap alphaUsd → base via DEX, and retry.
1115
+ const transferAmount = parseUnits('5', 6)
1116
+ const result = await fillTransaction(client, {
1117
+ account: sender.address,
1118
+ ...Actions.token.transfer.call(caller, {
1119
+ token: base,
1120
+ to: Relay.accounts[7]!.address,
1121
+ amount: transferAmount,
1122
+ }),
1123
+ })
1124
+
1125
+ // Should succeed — relay auto-swapped quote → base.
1126
+ const { transaction, capabilities } = result
1127
+ expect(transaction.gas).toBeDefined()
1128
+ expect(transaction.nonce).toBeDefined()
1129
+ expect(transaction.feeToken).toBe(Relay.addresses.alphaUsd)
1130
+ expect(transaction.calls).toHaveLength(3) // approve + swap + transfer
1131
+
1132
+ const m = capabilities
1133
+ expect(m?.['sponsored']).toBe(false)
1134
+ expect(m?.['fee']?.decimals).toBe(6)
1135
+ expect(m?.['fee']?.symbol).toBe('AlphaUSD')
1136
+
1137
+ // Balance diffs exclude swap tokens — only the user's transfer shows.
1138
+ const diffs = findDiffs(m?.['balanceDiffs'], sender.address)!
1139
+ expect(diffs).toHaveLength(1)
1140
+ expect(diffs[0]!.direction).toBe('outgoing')
1141
+ expect(diffs[0]!.formatted).toBe('5')
1142
+ expect(diffs[0]!.symbol).toBe('SWBASE')
1143
+ expect(diffs[0]!.address.toLowerCase()).toBe(base.toLowerCase())
1144
+
1145
+ // autoSwap reports the injected AMM swap.
1146
+ expect(m?.['autoSwap']?.slippage).toBe(0.05)
1147
+ expect(m?.['autoSwap']?.maxIn.formatted).toBe('5.25')
1148
+ expect(m?.['autoSwap']?.maxIn.symbol).toBe('AlphaUSD')
1149
+ expect(m?.['autoSwap']?.maxIn.token.toLowerCase()).toBe(Relay.addresses.alphaUsd.toLowerCase())
1150
+ expect(m?.['autoSwap']?.minOut.formatted).toBe('5')
1151
+ expect(m?.['autoSwap']?.minOut.symbol).toBe('SWBASE')
1152
+ expect(m?.['autoSwap']?.minOut.token.toLowerCase()).toBe(base.toLowerCase())
1153
+ })
1154
+
1155
+ test('behavior: custom slippage is applied to autoSwap', async () => {
1156
+ const sender = Relay.accounts[2]!
1157
+
1158
+ // Set up token pair + DEX liquidity.
1159
+ const rpc = Relay.getClient({ account: Relay.accounts[0]! })
1160
+ const { token: base } = await Actions.token.createSync(rpc, {
1161
+ name: 'Slippage Base',
1162
+ symbol: 'SLPBASE',
1163
+ currency: 'USD',
1164
+ quoteToken: Relay.addresses.alphaUsd,
1165
+ })
1166
+ await sendTransactionSync(rpc, {
1167
+ calls: [
1168
+ Actions.token.grantRoles.call(caller, {
1169
+ token: base,
1170
+ role: 'issuer',
1171
+ to: rpc.account!.address,
1172
+ }),
1173
+ Actions.token.mint.call(caller, {
1174
+ token: base,
1175
+ to: rpc.account!.address,
1176
+ amount: parseUnits('10000', 6),
1177
+ }),
1178
+ Actions.token.approve.call(caller, {
1179
+ token: base,
1180
+ spender: Addresses.stablecoinDex,
1181
+ amount: parseUnits('10000', 6),
1182
+ }),
1183
+ Actions.token.approve.call(caller, {
1184
+ token: Relay.addresses.alphaUsd,
1185
+ spender: Addresses.stablecoinDex,
1186
+ amount: parseUnits('10000', 6),
1187
+ }),
1188
+ ],
1189
+ })
1190
+ await Actions.dex.createPairSync(rpc, { base })
1191
+ await Actions.dex.placeSync(rpc, {
1192
+ token: base,
1193
+ amount: parseUnits('500', 6),
1194
+ type: 'sell',
1195
+ tick: Tick.fromPrice('1.001'),
1196
+ })
1197
+
1198
+ // Sender has faucet alphaUsd but NO base tokens.
1199
+ await Actions.fee.setUserToken(Relay.getClient({ account: sender }), {
1200
+ token: Relay.addresses.alphaUsd,
1201
+ })
1202
+
1203
+ // Create relay with custom 2% slippage.
1204
+ const customServer = await Relay.createServer(
1205
+ (
1206
+ await Relay.relay({
1207
+ features: 'all',
1208
+ autoSwap: { slippage: 0.02 },
1209
+ })
1210
+ ).listener,
1211
+ )
1212
+ const customClient = Relay.getClient({ url: customServer.url })
1213
+
1214
+ const result = await fillTransaction(customClient, {
1215
+ account: sender.address,
1216
+ ...Actions.token.transfer.call(caller, {
1217
+ token: base,
1218
+ to: Relay.accounts[7]!.address,
1219
+ amount: parseUnits('10', 6),
1220
+ }),
1221
+ })
1222
+ customServer.close()
1223
+
1224
+ const m = result.capabilities
1225
+ expect(m?.['autoSwap']?.slippage).toBe(0.02)
1226
+ // 10 + 2% = 10.2
1227
+ expect(m?.['autoSwap']?.maxIn.formatted).toBe('10.2')
1228
+ expect(m?.['autoSwap']?.minOut.formatted).toBe('10')
1229
+ })
1230
+
1231
+ test('behavior: autoSwap disabled throws InsufficientBalance instead of swapping', async () => {
1232
+ const sender = Relay.accounts[3]!
1233
+
1234
+ // Set up token pair + DEX liquidity.
1235
+ const rpc = Relay.getClient({ account: Relay.accounts[0]! })
1236
+ const { token: base } = await Actions.token.createSync(rpc, {
1237
+ name: 'No Swap Base',
1238
+ symbol: 'NSWBASE',
1239
+ currency: 'USD',
1240
+ quoteToken: Relay.addresses.alphaUsd,
1241
+ })
1242
+ await sendTransactionSync(rpc, {
1243
+ calls: [
1244
+ Actions.token.grantRoles.call(caller, {
1245
+ token: base,
1246
+ role: 'issuer',
1247
+ to: rpc.account!.address,
1248
+ }),
1249
+ Actions.token.mint.call(caller, {
1250
+ token: base,
1251
+ to: rpc.account!.address,
1252
+ amount: parseUnits('10000', 6),
1253
+ }),
1254
+ Actions.token.approve.call(caller, {
1255
+ token: base,
1256
+ spender: Addresses.stablecoinDex,
1257
+ amount: parseUnits('10000', 6),
1258
+ }),
1259
+ ],
1260
+ })
1261
+ await Actions.dex.createPairSync(rpc, { base })
1262
+ await Actions.dex.placeSync(rpc, {
1263
+ token: base,
1264
+ amount: parseUnits('500', 6),
1265
+ type: 'sell',
1266
+ tick: Tick.fromPrice('1.001'),
1267
+ })
1268
+
1269
+ // Sender has faucet alphaUsd but NO base tokens.
1270
+ await Actions.fee.setUserToken(Relay.getClient({ account: sender }), {
1271
+ token: Relay.addresses.alphaUsd,
1272
+ })
1273
+
1274
+ // Create relay with autoSwap disabled.
1275
+ const customServer = await Relay.createServer(
1276
+ (
1277
+ await Relay.relay({
1278
+ features: 'all',
1279
+ autoSwap: false,
1280
+ })
1281
+ ).listener,
1282
+ )
1283
+ const customClient = Relay.getClient({ url: customServer.url })
1284
+
1285
+ // Should return error capability instead of auto-swapping.
1286
+ const result = await fillTransaction(customClient, {
1287
+ account: sender.address,
1288
+ calls: [
1289
+ Actions.token.transfer.call(caller, {
1290
+ token: base,
1291
+ to: Relay.accounts[7]!.address,
1292
+ amount: parseUnits('5', 6),
1293
+ }),
1294
+ ],
1295
+ capabilities: { errors: true },
1296
+ })
1297
+ const error = result.capabilities?.['error']
1298
+ expect({ ...error, data: undefined }).toMatchInlineSnapshot(`
1299
+ {
1300
+ "abiItem": {
1301
+ "inputs": [
1302
+ {
1303
+ "name": "available",
1304
+ "type": "uint256",
1305
+ },
1306
+ {
1307
+ "name": "required",
1308
+ "type": "uint256",
1309
+ },
1310
+ {
1311
+ "name": "token",
1312
+ "type": "address",
1313
+ },
1314
+ ],
1315
+ "name": "InsufficientBalance",
1316
+ "type": "error",
1317
+ },
1318
+ "data": undefined,
1319
+ "errorName": "InsufficientBalance",
1320
+ "message": "Insufficient balance. Required: 5000000, available: 0.",
1321
+ }
1322
+ `)
1323
+ customServer.close()
1324
+ })
1325
+ })
1326
+
1327
+ describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: conditional sponsoring', () => {
1328
+ let server: Server
1329
+ let client: ReturnType<typeof Relay.getClient>
1330
+
1331
+ beforeAll(async () => {
1332
+ // accounts[3] is faucet-funded with alphaUsd so transfers succeed.
1333
+ await Actions.fee.setUserToken(Relay.getClient(), {
1334
+ account: Relay.accounts[3]!,
1335
+ token: Relay.addresses.alphaUsd,
1336
+ })
1337
+
1338
+ server = await Relay.createServer(
1339
+ (
1340
+ await Relay.relay({
1341
+ feePayer: {
1342
+ account: feePayerAccount,
1343
+ name: 'Test Sponsor',
1344
+ url: 'https://test.com',
1345
+ validate: (request) =>
1346
+ request.from?.toLowerCase() !== Relay.accounts[3]!.address.toLowerCase(),
1347
+ },
1348
+ })
1349
+ ).listener,
1350
+ )
1351
+ client = Relay.getClient({ url: server.url })
1352
+ })
1353
+
1354
+ afterAll(() => {
1355
+ server.close()
1356
+ })
1357
+
1358
+ test('behavior: approved tx is sponsored and can be broadcast', async () => {
1359
+ const { transaction } = await fillTransaction(client, {
1360
+ account: userAccount.address,
1361
+ calls: [transferCall()],
1362
+ })
1363
+ expect(transaction.feePayerSignature).toBeDefined()
1364
+
1365
+ const signed = await userAccount.signTransaction(transaction as never)
1366
+ const receipt = (await Relay.getClient().request({
1367
+ method: 'eth_sendRawTransactionSync' as never,
1368
+ params: [signed],
1369
+ })) as { feePayer?: string | undefined }
1370
+
1371
+ expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
1372
+ })
1373
+
1374
+ test('behavior: rejected tx is not sponsored and can be self-paid', async () => {
1375
+ const sender = Relay.accounts[3]!
1376
+ const result = await fillTransaction(client, {
1377
+ account: sender.address,
1378
+ calls: [transferCall()],
1379
+ })
1380
+ expect(result.transaction.feePayerSignature).toBeUndefined()
1381
+
1382
+ const meta = result.capabilities
1383
+ expect(meta?.['sponsored']).toBe(false)
1384
+ expect(meta?.['sponsor']).toBeUndefined()
1385
+
1386
+ const serialized = (await Transaction.serialize(result.transaction as never)) as `0x76${string}`
1387
+ const envelope = TxEnvelopeTempo.deserialize(serialized)
1388
+ const signature = await sender.sign({
1389
+ hash: TxEnvelopeTempo.getSignPayload(envelope),
1390
+ })
1391
+ const signed = TxEnvelopeTempo.serialize(envelope, {
1392
+ signature: SignatureEnvelope.from(signature),
1393
+ })
1394
+ const receipt = (await Relay.getClient().request({
1395
+ method: 'eth_sendRawTransactionSync' as never,
1396
+ params: [signed],
1397
+ })) as { feePayer?: string | undefined }
1398
+
1399
+ // Sender pays their own fee — no external fee payer.
1400
+ expect(receipt.feePayer).not.toBe(feePayerAccount.address.toLowerCase())
1401
+ })
1402
+ })
1403
+
1404
+ describe.skipIf(Runtime.get().mode !== 'localnet')(
1405
+ 'behavior: path A — guaranteed sponsorship (no validate)',
1406
+ () => {
1407
+ let server: Server
1408
+ let client: ReturnType<typeof Relay.getClient>
1409
+ let requests: RpcRequest.RpcRequest[] = []
1410
+
1411
+ beforeAll(async () => {
1412
+ server = await Relay.createServer(
1413
+ (
1414
+ await Relay.relay({
1415
+ features: 'all',
1416
+ tokens: localnetTokens,
1417
+ feePayer: {
1418
+ account: feePayerAccount,
1419
+ name: 'Path A Sponsor',
1420
+ },
1421
+ onRequest: async (request) => {
1422
+ requests.push(request)
1423
+ },
1424
+ })
1425
+ ).listener,
1426
+ )
1427
+ client = Relay.getClient({ url: server.url })
1428
+ })
1429
+
1430
+ afterAll(() => {
1431
+ server.close()
1432
+ })
1433
+
1434
+ afterEach(() => {
1435
+ requests = []
1436
+ })
1437
+
1438
+ test('behavior: skips fee token resolution and sponsors', async () => {
1439
+ const result = await fillTransaction(client, {
1440
+ account: userAccount.address,
1441
+ calls: [transferCall()],
1442
+ })
1443
+
1444
+ expect(result.transaction.feePayerSignature).toBeDefined()
1445
+ expect(result.capabilities?.['sponsored']).toBe(true)
1446
+ expect(result.capabilities?.['sponsor']?.name).toBe('Path A Sponsor')
1447
+ // Only one fill request — no fee token resolution round-trip.
1448
+ expect(requests.filter((r) => r.method === 'eth_fillTransaction')).toHaveLength(1)
1449
+ })
1450
+
1451
+ test('behavior: returns fee even when no feeToken in request', async () => {
1452
+ const result = await fillTransaction(client, {
1453
+ account: userAccount.address,
1454
+ calls: [transferCall()],
1455
+ })
1456
+
1457
+ expect(result.capabilities?.['fee']).toBeDefined()
1458
+ expect(result.capabilities?.['fee']?.decimals).toBeTypeOf('number')
1459
+ expect(result.capabilities?.['fee']?.symbol).toBeTypeOf('string')
1460
+ expect(result.capabilities?.['fee']?.formatted).toBeTypeOf('string')
1461
+ })
1462
+
1463
+ test('behavior: simulate and sign run concurrently with fill', async () => {
1464
+ const result = await fillTransaction(client, {
1465
+ account: userAccount.address,
1466
+ calls: [transferCall()],
1467
+ })
1468
+
1469
+ // All capabilities are present despite parallel execution.
1470
+ expect(result.transaction.feePayerSignature).toBeDefined()
1471
+ expect(result.capabilities?.['sponsored']).toBe(true)
1472
+ })
1473
+
1474
+ test('behavior: defaults feeToken to chain default when caller omits it', async () => {
1475
+ // Without the default, the broadcast envelope has no feeToken and
1476
+ // the chain falls back to the sender's account token, which often
1477
+ // lacks FeeAMM liquidity.
1478
+ const result = await fillTransaction(client, {
1479
+ account: userAccount.address,
1480
+ calls: [transferCall()],
1481
+ })
1482
+
1483
+ expect(result.transaction.feeToken?.toLowerCase()).toBe(
1484
+ localnetTokens[0]!.address.toLowerCase(),
1485
+ )
1486
+ })
1487
+
1488
+ test('behavior: preserves caller-supplied feeToken', async () => {
1489
+ const result = await fillTransaction(client, {
1490
+ account: userAccount.address,
1491
+ calls: [transferCall()],
1492
+ feeToken: Relay.addresses.alphaUsd as Address,
1493
+ })
1494
+
1495
+ expect(result.transaction.feeToken?.toLowerCase()).toBe(
1496
+ Relay.addresses.alphaUsd.toLowerCase(),
1497
+ )
1498
+ })
1499
+ },
1500
+ )
1501
+
1502
+ describe.skipIf(Runtime.get().mode !== 'localnet')(
1503
+ 'behavior: path B — conditional sponsorship (validate)',
1504
+ () => {
1505
+ let server: Server
1506
+ let client: ReturnType<typeof Relay.getClient>
1507
+ let requests: RpcRequest.RpcRequest[] = []
1508
+
1509
+ // Reject accounts[3], approve everyone else.
1510
+ const rejectedSender = Relay.accounts[3]!
1511
+
1512
+ beforeAll(async () => {
1513
+ // rejectedSender is faucet-funded with alphaUsd so it can self-pay.
1514
+ await Actions.fee.setUserToken(Relay.getClient(), {
1515
+ account: rejectedSender,
1516
+ token: Relay.addresses.alphaUsd,
1517
+ })
1518
+
1519
+ server = await Relay.createServer(
1520
+ (
1521
+ await Relay.relay({
1522
+ features: 'all',
1523
+ feePayer: {
1524
+ account: feePayerAccount,
1525
+ name: 'Path B Sponsor',
1526
+ validate: (request) =>
1527
+ request.from?.toLowerCase() !== rejectedSender.address.toLowerCase(),
1528
+ },
1529
+ onRequest: async (request) => {
1530
+ requests.push(request)
1531
+ },
1532
+ })
1533
+ ).listener,
1534
+ )
1535
+ client = Relay.getClient({ url: server.url })
1536
+ })
1537
+
1538
+ afterAll(() => {
1539
+ server.close()
1540
+ })
1541
+
1542
+ afterEach(() => {
1543
+ requests = []
1544
+ })
1545
+
1546
+ test('behavior: approved sender gets sponsorship with parallel fills', async () => {
1547
+ const result = await fillTransaction(client, {
1548
+ account: userAccount.address,
1549
+ calls: [transferCall()],
1550
+ })
1551
+
1552
+ expect(result.transaction.feePayerSignature).toBeDefined()
1553
+ expect(result.capabilities?.['sponsored']).toBe(true)
1554
+ expect(result.capabilities?.['sponsor']?.name).toBe('Path B Sponsor')
1555
+ })
1556
+
1557
+ test('behavior: rejected sender gets unsponsored tx from parallel fill', async () => {
1558
+ const result = await fillTransaction(client, {
1559
+ account: rejectedSender.address,
1560
+ calls: [transferCall()],
1561
+ })
1562
+
1563
+ expect(result.transaction.feePayerSignature).toBeUndefined()
1564
+ expect(result.capabilities?.['sponsored']).toBe(false)
1565
+ expect(result.capabilities?.['sponsor']).toBeUndefined()
1566
+ })
1567
+
1568
+ test('behavior: rejected tx can be signed and broadcast by sender', async () => {
1569
+ const result = await fillTransaction(client, {
1570
+ account: rejectedSender.address,
1571
+ calls: [transferCall()],
1572
+ })
1573
+
1574
+ const serialized = (await Transaction.serialize(
1575
+ result.transaction as never,
1576
+ )) as `0x76${string}`
1577
+ const envelope = TxEnvelopeTempo.deserialize(serialized)
1578
+ const signature = await rejectedSender.sign({
1579
+ hash: TxEnvelopeTempo.getSignPayload(envelope),
1580
+ })
1581
+ const signed = TxEnvelopeTempo.serialize(envelope, {
1582
+ signature: SignatureEnvelope.from(signature),
1583
+ })
1584
+ const receipt = (await Relay.getClient().request({
1585
+ method: 'eth_sendRawTransactionSync' as never,
1586
+ params: [signed],
1587
+ })) as { feePayer?: string | undefined }
1588
+
1589
+ expect(receipt.feePayer).not.toBe(feePayerAccount.address.toLowerCase())
1590
+ })
1591
+ },
1592
+ )
1593
+
1594
+ describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: path C — no sponsorship', () => {
1595
+ let server: Server
1596
+ let client: ReturnType<typeof Relay.getClient>
1597
+ let requests: RpcRequest.RpcRequest[] = []
1598
+
1599
+ beforeAll(async () => {
1600
+ server = await Relay.createServer(
1601
+ (
1602
+ await Relay.relay({
1603
+ features: 'all',
1604
+ onRequest: async (request) => {
1605
+ requests.push(request)
1606
+ },
1607
+ })
1608
+ ).listener,
1609
+ )
1610
+ client = Relay.getClient({ url: server.url })
1611
+ })
1612
+
1613
+ afterAll(() => {
1614
+ server.close()
1615
+ })
1616
+
1617
+ afterEach(() => {
1618
+ requests = []
1619
+ })
1620
+
1621
+ test('behavior: resolves fee token and fills without sponsorship', async () => {
1622
+ const result = await fillTransaction(client, {
1623
+ account: userAccount.address,
1624
+ calls: [transferCall()],
1625
+ })
1626
+
1627
+ expect(result.transaction.feePayerSignature).toBeUndefined()
1628
+ expect(result.capabilities?.['sponsored']).toBe(false)
1629
+ expect(result.capabilities?.['fee']).toBeDefined()
1630
+ // Single fill — no speculative second fill.
1631
+ expect(requests.filter((r) => r.method === 'eth_fillTransaction')).toHaveLength(1)
1632
+ })
1633
+
1634
+ test('behavior: simulate and autoSwap metadata resolve concurrently', async () => {
1635
+ const result = await fillTransaction(client, {
1636
+ account: userAccount.address,
1637
+ calls: [transferCall()],
1638
+ })
1639
+
1640
+ // Capabilities are populated despite parallel execution.
1641
+ expect(result.transaction.gas).toBeDefined()
1642
+ expect(result.transaction.nonce).toBeDefined()
1643
+ expect(result.capabilities?.['fee']).toBeDefined()
1644
+ expect(result.capabilities?.['sponsored']).toBe(false)
1645
+ })
1646
+ })
1647
+
1648
+ describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: fee token resolution', () => {
1649
+ const feeTokenAccount = Relay.accounts[0]!
1650
+ const preferredToken = Relay.addresses.alphaUsd
1651
+ let server: Server
1652
+ let client: ReturnType<typeof Relay.getClient>
1653
+
1654
+ beforeAll(async () => {
1655
+ // feeTokenAccount is faucet-funded with alphaUsd so the balance check passes.
1656
+ // Set on-chain fee token preference.
1657
+ await Actions.fee.setUserToken(Relay.getClient(), {
1658
+ account: feeTokenAccount,
1659
+ token: preferredToken,
1660
+ })
1661
+
1662
+ server = await Relay.createServer(
1663
+ (
1664
+ await Relay.relay({
1665
+ features: 'all',
1666
+ tokens: localnetTokens,
1667
+ })
1668
+ ).listener,
1669
+ )
1670
+ client = Relay.getClient({ url: server.url })
1671
+ })
1672
+
1673
+ afterAll(() => {
1674
+ server.close()
1675
+ })
1676
+
1677
+ test('behavior: uses explicitly provided feeToken', async () => {
1678
+ const feeToken = '0x20c0000000000000000000000000000000000001'
1679
+ const { transaction } = await fillTransaction(client, {
1680
+ account: feeTokenAccount.address,
1681
+ calls: [transferCall()],
1682
+ feeToken,
1683
+ })
1684
+
1685
+ expect(transaction.feeToken).toBe(feeToken)
1686
+ })
1687
+
1688
+ test('behavior: resolves to onchain user token when it has balance', async () => {
1689
+ const { transaction } = await fillTransaction(client, {
1690
+ account: feeTokenAccount.address,
1691
+ calls: [transferCall()],
1692
+ })
1693
+
1694
+ expect(transaction.feeToken).toBe(preferredToken)
1695
+ })
1696
+
1697
+ test('behavior: resolves to highest-balance token from token list', async () => {
1698
+ // Create two fresh TIP-20s and mint different amounts to an account that is
1699
+ // NOT faucet-funded, so only these two balances influence resolution
1700
+ // (a faucet-funded account would carry genesis balances that dominate).
1701
+ // The relay's tokenlist is restricted to just these two so the
1702
+ // highest-balance resolver picks between them deterministically.
1703
+ const freshAccount = Relay.accounts[5]!
1704
+ const rpc = Relay.getClient({ account: Relay.accounts[0]! })
1705
+ const { token: lowUsd } = await Actions.token.createSync(rpc, {
1706
+ name: 'LowUSD',
1707
+ symbol: 'LowUSD',
1708
+ currency: 'USD',
1709
+ quoteToken: Relay.addresses.alphaUsd,
1710
+ })
1711
+ const { token: highUsd } = await Actions.token.createSync(rpc, {
1712
+ name: 'HighUSD',
1713
+ symbol: 'HighUSD',
1714
+ currency: 'USD',
1715
+ quoteToken: Relay.addresses.alphaUsd,
1716
+ })
1717
+ await sendTransactionSync(rpc, {
1718
+ calls: [
1719
+ Actions.token.grantRoles.call(caller, {
1720
+ token: lowUsd,
1721
+ role: 'issuer',
1722
+ to: rpc.account!.address,
1723
+ }),
1724
+ Actions.token.grantRoles.call(caller, {
1725
+ token: highUsd,
1726
+ role: 'issuer',
1727
+ to: rpc.account!.address,
1728
+ }),
1729
+ Actions.token.mint.call(caller, {
1730
+ token: lowUsd,
1731
+ amount: parseUnits('100', 6),
1732
+ to: freshAccount.address,
1733
+ }),
1734
+ Actions.token.mint.call(caller, {
1735
+ token: highUsd,
1736
+ amount: parseUnits('500', 6),
1737
+ to: freshAccount.address,
1738
+ }),
1739
+ ],
1740
+ })
1741
+
1742
+ const customServer = await Relay.createServer(
1743
+ (
1744
+ await Relay.relay({
1745
+ features: 'all',
1746
+ tokens: [
1747
+ { address: lowUsd, decimals: 6, name: 'LowUSD', symbol: 'LowUSD' },
1748
+ { address: highUsd, decimals: 6, name: 'HighUSD', symbol: 'HighUSD' },
1749
+ ],
1750
+ })
1751
+ ).listener,
1752
+ )
1753
+ const customClient = Relay.getClient({ url: customServer.url })
1754
+
1755
+ const { transaction } = await fillTransaction(customClient, {
1756
+ account: freshAccount.address,
1757
+ calls: [
1758
+ Actions.token.transfer.call(caller, { token: highUsd, to: recipient.address, amount: 1n }),
1759
+ ],
1760
+ })
1761
+ customServer.close()
1762
+
1763
+ // highUsd has the higher balance (500 > 100).
1764
+ expect(transaction.feeToken?.toLowerCase()).toBe(highUsd.toLowerCase())
1765
+ })
1766
+
1767
+ test('behavior: falls back to pathUSD when no preference or balances', async () => {
1768
+ const freshAccount = Relay.accounts[10]!
1769
+ const { transaction } = await fillTransaction(client, {
1770
+ account: freshAccount.address,
1771
+ calls: [
1772
+ Actions.token.transfer.call(caller, {
1773
+ token: Relay.addresses.alphaUsd,
1774
+ to: freshAccount.address,
1775
+ amount: 0n,
1776
+ }),
1777
+ ],
1778
+ })
1779
+
1780
+ expect(transaction.feeToken).toBeUndefined()
1781
+ })
1782
+ })
1783
+
1784
+ describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: error capabilities', () => {
1785
+ let server: Server
1786
+ let client: ReturnType<typeof Relay.getClient>
1787
+
1788
+ beforeAll(async () => {
1789
+ server = await Relay.createServer(
1790
+ (
1791
+ await Relay.relay({
1792
+ features: 'all',
1793
+ getClient: Relay.readClient('https://rpc.moderato.tempo.xyz', tempoModerato.id),
1794
+ })
1795
+ ).listener,
1796
+ )
1797
+ client = Relay.getClient({ url: server.url })
1798
+ })
1799
+
1800
+ afterAll(() => {
1801
+ server.close()
1802
+ })
1803
+
1804
+ // Skipped: this asserts an inline snapshot of the sender's live moderato
1805
+ // `balanceDiffs`, which depends on that account's balances on the hosted
1806
+ // moderato testnet at capture time — not reproducible in the disposable
1807
+ // localnet harness. The sibling error/JSON-RPC error tests below cover the
1808
+ // same relay error-capability behavior without a live-balance snapshot.
1809
+ test.skip('behavior: returns requireFunds on InsufficientBalance when errors capability is enabled', async () => {
1810
+ const sender = Relay.accounts[10]!
1811
+
1812
+ const result = await fillTransaction(client, {
1813
+ account: sender.address,
1814
+ calls: [
1815
+ Actions.token.transfer.call(caller, {
1816
+ token: Relay.addresses.alphaUsd,
1817
+ to: recipient.address,
1818
+ amount: parseUnits('100', 6),
1819
+ }),
1820
+ ],
1821
+ capabilities: { errors: true },
1822
+ })
1823
+
1824
+ expect(result.capabilities).toMatchInlineSnapshot(`
1825
+ {
1826
+ "balanceDiffs": {
1827
+ "0x0eB552e73e6f8E0922749e0fB08af2a71ECb2b7F": [
1828
+ {
1829
+ "address": "0x20c0000000000000000000000000000000000001",
1830
+ "decimals": 6,
1831
+ "direction": "outgoing",
1832
+ "formatted": "100",
1833
+ "name": "AlphaUSD",
1834
+ "recipients": [
1835
+ "0xAF4311d557fBC876059e39306ec1f3343753df29",
1836
+ ],
1837
+ "symbol": "AlphaUSD",
1838
+ "value": "0x5f5e100",
1839
+ },
1840
+ ],
1841
+ },
1842
+ "error": {
1843
+ "abiItem": {
1844
+ "inputs": [
1845
+ {
1846
+ "name": "available",
1847
+ "type": "uint256",
1848
+ },
1849
+ {
1850
+ "name": "required",
1851
+ "type": "uint256",
1852
+ },
1853
+ {
1854
+ "name": "token",
1855
+ "type": "address",
1856
+ },
1857
+ ],
1858
+ "name": "InsufficientBalance",
1859
+ "type": "error",
1860
+ },
1861
+ "data": "0x832f98b500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005f5e10000000000000000000000000020c0000000000000000000000000000000000001",
1862
+ "errorName": "InsufficientBalance",
1863
+ "message": "Insufficient balance. Required: 100000000, available: 0.",
1864
+ },
1865
+ "requireFunds": {
1866
+ "amount": "0x5f5e100",
1867
+ "decimals": 6,
1868
+ "formatted": "100",
1869
+ "symbol": "AlphaUSD",
1870
+ "token": "0x20C0000000000000000000000000000000000001",
1871
+ },
1872
+ "sponsored": false,
1873
+ }
1874
+ `)
1875
+ })
1876
+
1877
+ test('behavior: returns error capability on generic revert when errors capability is enabled', async () => {
1878
+ const sender = Relay.accounts[10]!
1879
+
1880
+ const result = await fillTransaction(client, {
1881
+ account: sender.address,
1882
+ calls: [
1883
+ Actions.token.grantRoles.call(caller, {
1884
+ token: Relay.addresses.alphaUsd,
1885
+ role: 'issuer',
1886
+ to: sender.address,
1887
+ }),
1888
+ ],
1889
+ capabilities: { errors: true },
1890
+ })
1891
+
1892
+ expect(result.capabilities).toMatchInlineSnapshot(`
1893
+ {
1894
+ "error": {
1895
+ "abiItem": {
1896
+ "inputs": [],
1897
+ "name": "Unauthorized",
1898
+ "type": "error",
1899
+ },
1900
+ "data": "0x82b42900",
1901
+ "errorName": "Unauthorized",
1902
+ "message": "Unauthorized.",
1903
+ },
1904
+ "sponsored": false,
1905
+ }
1906
+ `)
1907
+ })
1908
+
1909
+ test('default: throws JSON-RPC error on InsufficientBalance', async () => {
1910
+ const sender = Relay.accounts[10]!
1911
+ const error = await fillTransaction(client, {
1912
+ account: sender.address,
1913
+ calls: [
1914
+ Actions.token.transfer.call(caller, {
1915
+ token: Relay.addresses.alphaUsd,
1916
+ to: recipient.address,
1917
+ amount: parseUnits('100', 6),
1918
+ }),
1919
+ ],
1920
+ }).then(
1921
+ () => undefined,
1922
+ (e: BaseError) => e,
1923
+ )
1924
+ expect(error).toBeDefined()
1925
+ // Walk the viem error chain to the underlying RPC error. The default path
1926
+ // surfaces the chain revert as a JSON-RPC error (`code: 3`) with the
1927
+ // ABI-encoded `InsufficientBalance(uint256, uint256, address)` selector.
1928
+ const rpc = error?.walk(isRpcError) as { data?: string } | undefined
1929
+ expect(rpc?.data?.startsWith('0x832f98b5')).toBe(true)
1930
+ })
1931
+
1932
+ test('default: throws JSON-RPC error on generic revert', async () => {
1933
+ const sender = Relay.accounts[10]!
1934
+ const error = await fillTransaction(client, {
1935
+ account: sender.address,
1936
+ calls: [
1937
+ Actions.token.grantRoles.call(caller, {
1938
+ token: Relay.addresses.alphaUsd,
1939
+ role: 'issuer',
1940
+ to: sender.address,
1941
+ }),
1942
+ ],
1943
+ }).then(
1944
+ () => undefined,
1945
+ (e: BaseError) => e,
1946
+ )
1947
+ expect(error).toBeDefined()
1948
+ const rpc = error?.walk(isRpcError) as { data?: string } | undefined
1949
+ // ABI-encoded `Unauthorized()`.
1950
+ expect(rpc?.data).toBe('0x82b42900')
1951
+ })
1952
+
1953
+ test('behavior: explicit `errors: false` matches default JSON-RPC error behavior', async () => {
1954
+ const sender = Relay.accounts[10]!
1955
+ const error = await fillTransaction(client, {
1956
+ account: sender.address,
1957
+ calls: [
1958
+ Actions.token.transfer.call(caller, {
1959
+ token: Relay.addresses.alphaUsd,
1960
+ to: recipient.address,
1961
+ amount: parseUnits('100', 6),
1962
+ }),
1963
+ ],
1964
+ capabilities: { errors: false } as never,
1965
+ }).then(
1966
+ () => undefined,
1967
+ (e: BaseError) => e,
1968
+ )
1969
+ expect(error).toBeDefined()
1970
+ const rpc = error?.walk(isRpcError) as { data?: string } | undefined
1971
+ expect(rpc?.data?.startsWith('0x832f98b5')).toBe(true)
1972
+ })
1973
+ })
1974
+
1975
+ function isRpcError(e: unknown): boolean {
1976
+ return typeof e === 'object' && e !== null && 'code' in e && (e as { code: unknown }).code === 3
1977
+ }
1978
+
1979
+ // Skipped: this drives a real mainnet fill against the hosted presto RPC using
1980
+ // a hard-coded sender that must hold live USDC.e (and no PathUSD) for autoSwap
1981
+ // to trigger — external state that isn't reproducible in the localnet harness.
1982
+ describe.skip('behavior: mainnet autoSwap with USDC.e → PathUSD', () => {
1983
+ let server: Server
1984
+ let mainnetClient: ReturnType<typeof Relay.getClient>
1985
+
1986
+ const mainnetUsdce = '0x20c000000000000000000000b9537d11c60e8b50' as const
1987
+ const mainnetPathUsd = '0x20c0000000000000000000000000000000000000' as const
1988
+ const mainnetSender = '0xb472f3ca15f34Db22d43FA503043F1e6541AC085' as const
1989
+
1990
+ beforeAll(async () => {
1991
+ server = await Relay.createServer(
1992
+ (
1993
+ await Relay.relay({
1994
+ features: 'all',
1995
+ getClient: Relay.readClient('https://rpc.presto.tempo.xyz', tempo.id),
1996
+ })
1997
+ ).listener,
1998
+ )
1999
+ mainnetClient = Relay.getClient({ url: server.url })
2000
+ })
2001
+
2002
+ afterAll(() => {
2003
+ server.close()
2004
+ })
2005
+
2006
+ test('behavior: auto-swaps USDC.e → PathUSD when sender has USDC.e but no PathUSD', async () => {
2007
+ const result = await fillTransaction(mainnetClient, {
2008
+ account: mainnetSender,
2009
+ calls: [
2010
+ Actions.token.transfer.call(caller, {
2011
+ token: mainnetPathUsd,
2012
+ to: recipient.address,
2013
+ amount: parseUnits('0.5', 6),
2014
+ }),
2015
+ ],
2016
+ })
2017
+
2018
+ // Expected: relay detects InsufficientBalance for PathUSD, auto-swaps
2019
+ // USDC.e → PathUSD via DEX, and returns a filled transaction with swap calls.
2020
+ const { transaction, capabilities } = result
2021
+ expect(transaction.gas).toBeDefined()
2022
+ expect(transaction.nonce).toBeDefined()
2023
+ expect(transaction.calls!.length).toBeGreaterThanOrEqual(3) // approve + swap + transfer
2024
+
2025
+ expect(capabilities?.['autoSwap']).toBeDefined()
2026
+ expect(capabilities?.['autoSwap']?.maxIn.token.toLowerCase()).toBe(mainnetUsdce.toLowerCase())
2027
+ expect(capabilities?.['autoSwap']?.minOut.token.toLowerCase()).toBe(
2028
+ mainnetPathUsd.toLowerCase(),
2029
+ )
2030
+ expect(capabilities?.['sponsored']).toBe(false)
2031
+
2032
+ // Should NOT have requireFunds — autoSwap should handle it.
2033
+ expect((capabilities as Record<string, unknown>)?.['requireFunds']).toBeUndefined()
2034
+ })
2035
+ })