tapimo 0.1.0 → 0.3.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 (520) hide show
  1. package/README.md +57 -16
  2. package/dist/ApiKey.d.ts +4 -1
  3. package/dist/ApiKey.d.ts.map +1 -1
  4. package/dist/ApiKey.js +6 -2
  5. package/dist/ApiKey.js.map +1 -1
  6. package/dist/ApiKeys.d.ts +98 -3
  7. package/dist/ApiKeys.d.ts.map +1 -1
  8. package/dist/ApiKeys.js +150 -1
  9. package/dist/ApiKeys.js.map +1 -1
  10. package/dist/App.d.ts +7917 -2315
  11. package/dist/App.d.ts.map +1 -1
  12. package/dist/App.js +141 -329
  13. package/dist/App.js.map +1 -1
  14. package/dist/Scope.d.ts +20 -4
  15. package/dist/Scope.d.ts.map +1 -1
  16. package/dist/Scope.js +23 -4
  17. package/dist/Scope.js.map +1 -1
  18. package/dist/Webhooks.d.ts +2 -2
  19. package/dist/Webhooks.d.ts.map +1 -1
  20. package/dist/Webhooks.js +2 -2
  21. package/dist/Webhooks.js.map +1 -1
  22. package/dist/admin/App.d.ts +203 -120
  23. package/dist/admin/App.d.ts.map +1 -1
  24. package/dist/admin/apps/api-keys.d.ts +111 -6
  25. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  26. package/dist/admin/apps/api-keys.js +67 -0
  27. package/dist/admin/apps/api-keys.js.map +1 -1
  28. package/dist/admin/apps/scopes.d.ts +12 -1
  29. package/dist/admin/apps/scopes.d.ts.map +1 -1
  30. package/dist/admin/apps/scopes.js +3 -0
  31. package/dist/admin/apps/scopes.js.map +1 -1
  32. package/dist/admin/apps/verified-tokens.d.ts +116 -116
  33. package/dist/admin/apps/verified-tokens.js +1 -1
  34. package/dist/admin/apps/verified-tokens.js.map +1 -1
  35. package/dist/admin/ui.generated.d.ts +1 -1
  36. package/dist/admin/ui.generated.js +13 -13
  37. package/dist/admin/ui.generated.js.map +1 -1
  38. package/dist/analytics/Analytics.d.ts +151 -0
  39. package/dist/analytics/Analytics.d.ts.map +1 -0
  40. package/dist/analytics/Analytics.js +212 -0
  41. package/dist/analytics/Analytics.js.map +1 -0
  42. package/dist/analytics/tables/requestEvents.d.ts +215 -0
  43. package/dist/analytics/tables/requestEvents.d.ts.map +1 -0
  44. package/dist/analytics/tables/requestEvents.js +264 -0
  45. package/dist/analytics/tables/requestEvents.js.map +1 -0
  46. package/dist/apps/data/App.d.ts +19576 -0
  47. package/dist/apps/data/App.d.ts.map +1 -0
  48. package/dist/apps/data/App.js +272 -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} +4 -4
  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} +8 -8
  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} +108 -108
  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} +39 -39
  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/data/routes/funding.d.ts +765 -0
  83. package/dist/apps/data/routes/funding.d.ts.map +1 -0
  84. package/dist/apps/data/routes/funding.js +383 -0
  85. package/dist/apps/data/routes/funding.js.map +1 -0
  86. package/dist/apps/{indexer.d.ts → data/routes/indexer.d.ts} +4 -4
  87. package/dist/apps/data/routes/indexer.d.ts.map +1 -0
  88. package/dist/apps/{indexer.js → data/routes/indexer.js} +13 -9
  89. package/dist/apps/data/routes/indexer.js.map +1 -0
  90. package/dist/apps/{receipts.d.ts → data/routes/receipts.d.ts} +49 -49
  91. package/dist/apps/data/routes/receipts.d.ts.map +1 -0
  92. package/dist/apps/{receipts.js → data/routes/receipts.js} +14 -20
  93. package/dist/apps/data/routes/receipts.js.map +1 -0
  94. package/dist/apps/{rpc.d.ts → data/routes/rpc.d.ts} +33 -40
  95. package/dist/apps/data/routes/rpc.d.ts.map +1 -0
  96. package/dist/apps/{rpc.js → data/routes/rpc.js} +35 -7
  97. package/dist/apps/data/routes/rpc.js.map +1 -0
  98. package/dist/apps/{tokenlist.d.ts → data/routes/tokenlist.d.ts} +40 -40
  99. package/dist/apps/data/routes/tokenlist.d.ts.map +1 -0
  100. package/dist/apps/{tokenlist.js → data/routes/tokenlist.js} +8 -8
  101. package/dist/apps/data/routes/tokenlist.js.map +1 -0
  102. package/dist/apps/{tokens.d.ts → data/routes/tokens.d.ts} +468 -468
  103. package/dist/apps/data/routes/tokens.d.ts.map +1 -0
  104. package/dist/apps/{tokens.js → data/routes/tokens.js} +19 -19
  105. package/dist/apps/data/routes/tokens.js.map +1 -0
  106. package/dist/apps/{transactions.d.ts → data/routes/transactions.d.ts} +304 -304
  107. package/dist/apps/data/routes/transactions.d.ts.map +1 -0
  108. package/dist/apps/{transactions.js → data/routes/transactions.js} +13 -13
  109. package/dist/apps/data/routes/transactions.js.map +1 -0
  110. package/dist/apps/{transfers.d.ts → data/routes/transfers.d.ts} +62 -62
  111. package/dist/apps/data/routes/transfers.d.ts.map +1 -0
  112. package/dist/apps/{transfers.js → data/routes/transfers.js} +13 -13
  113. package/dist/apps/data/routes/transfers.js.map +1 -0
  114. package/dist/apps/{verified-tokens.d.ts → data/routes/verified-tokens.d.ts} +6 -6
  115. package/dist/apps/data/routes/verified-tokens.d.ts.map +1 -0
  116. package/dist/apps/{verified-tokens.js → data/routes/verified-tokens.js} +9 -9
  117. package/dist/apps/data/routes/verified-tokens.js.map +1 -0
  118. package/dist/apps/{webhooks.d.ts → data/routes/webhooks.d.ts} +17 -17
  119. package/dist/apps/{webhooks.d.ts.map → data/routes/webhooks.d.ts.map} +1 -1
  120. package/dist/apps/{webhooks.js → data/routes/webhooks.js} +38 -34
  121. package/dist/apps/data/routes/webhooks.js.map +1 -0
  122. package/dist/apps/docs/App.d.ts +29 -0
  123. package/dist/apps/docs/App.d.ts.map +1 -0
  124. package/dist/apps/docs/App.js +54 -0
  125. package/dist/apps/docs/App.js.map +1 -0
  126. package/dist/{internal/OpenApi.fonts.d.ts → apps/docs/fonts.d.ts} +1 -1
  127. package/dist/apps/docs/fonts.d.ts.map +1 -0
  128. package/dist/{internal/OpenApi.fonts.js → apps/docs/fonts.js} +1 -1
  129. package/dist/apps/docs/fonts.js.map +1 -0
  130. package/dist/apps/docs/index.d.ts +2 -0
  131. package/dist/apps/docs/index.d.ts.map +1 -0
  132. package/dist/apps/docs/index.js +2 -0
  133. package/dist/apps/docs/index.js.map +1 -0
  134. package/dist/{internal/OpenApi.logo.d.ts → apps/docs/logo.d.ts} +1 -1
  135. package/dist/apps/docs/logo.d.ts.map +1 -0
  136. package/dist/{internal/OpenApi.logo.js → apps/docs/logo.js} +1 -1
  137. package/dist/apps/docs/logo.js.map +1 -0
  138. package/dist/apps/docs/reference.d.ts +21 -0
  139. package/dist/apps/docs/reference.d.ts.map +1 -0
  140. package/dist/apps/docs/reference.js +112 -0
  141. package/dist/apps/docs/reference.js.map +1 -0
  142. package/dist/apps/index.d.ts +5 -0
  143. package/dist/apps/index.d.ts.map +1 -0
  144. package/dist/apps/index.js +5 -0
  145. package/dist/apps/index.js.map +1 -0
  146. package/dist/apps/mcp.d.ts +76 -0
  147. package/dist/apps/mcp.d.ts.map +1 -0
  148. package/dist/apps/mcp.js +188 -0
  149. package/dist/apps/mcp.js.map +1 -0
  150. package/dist/apps/metadata.d.ts +7 -0
  151. package/dist/apps/metadata.d.ts.map +1 -0
  152. package/dist/apps/metadata.js +15 -0
  153. package/dist/apps/metadata.js.map +1 -0
  154. package/dist/apps/platform/App.d.ts +5262 -0
  155. package/dist/apps/platform/App.d.ts.map +1 -0
  156. package/dist/apps/platform/App.js +84 -0
  157. package/dist/apps/platform/App.js.map +1 -0
  158. package/dist/apps/platform/Billing.d.ts +279 -0
  159. package/dist/apps/platform/Billing.d.ts.map +1 -0
  160. package/dist/apps/platform/Billing.js +477 -0
  161. package/dist/apps/platform/Billing.js.map +1 -0
  162. package/dist/apps/platform/index.d.ts +3 -0
  163. package/dist/apps/platform/index.d.ts.map +1 -0
  164. package/dist/apps/platform/index.js +3 -0
  165. package/dist/apps/platform/index.js.map +1 -0
  166. package/dist/apps/platform/routes/api-keys.d.ts +973 -0
  167. package/dist/apps/platform/routes/api-keys.d.ts.map +1 -0
  168. package/dist/apps/platform/routes/api-keys.js +430 -0
  169. package/dist/apps/platform/routes/api-keys.js.map +1 -0
  170. package/dist/apps/platform/routes/billing.d.ts +1520 -0
  171. package/dist/apps/platform/routes/billing.d.ts.map +1 -0
  172. package/dist/apps/platform/routes/billing.js +795 -0
  173. package/dist/apps/platform/routes/billing.js.map +1 -0
  174. package/dist/apps/platform/routes/invitations.d.ts +851 -0
  175. package/dist/apps/platform/routes/invitations.d.ts.map +1 -0
  176. package/dist/apps/platform/routes/invitations.js +471 -0
  177. package/dist/apps/platform/routes/invitations.js.map +1 -0
  178. package/dist/apps/platform/routes/me.d.ts +95 -0
  179. package/dist/apps/platform/routes/me.d.ts.map +1 -0
  180. package/dist/apps/platform/routes/me.js +81 -0
  181. package/dist/apps/platform/routes/me.js.map +1 -0
  182. package/dist/apps/platform/routes/members.d.ts +554 -0
  183. package/dist/apps/platform/routes/members.d.ts.map +1 -0
  184. package/dist/apps/platform/routes/members.js +251 -0
  185. package/dist/apps/platform/routes/members.js.map +1 -0
  186. package/dist/apps/platform/routes/orgs.d.ts +596 -0
  187. package/dist/apps/platform/routes/orgs.d.ts.map +1 -0
  188. package/dist/apps/platform/routes/orgs.js +276 -0
  189. package/dist/apps/platform/routes/orgs.js.map +1 -0
  190. package/dist/apps/platform/routes/projects.d.ts +740 -0
  191. package/dist/apps/platform/routes/projects.d.ts.map +1 -0
  192. package/dist/apps/platform/routes/projects.js +286 -0
  193. package/dist/apps/platform/routes/projects.js.map +1 -0
  194. package/dist/apps/platform/routes/scopes.d.ts +107 -0
  195. package/dist/apps/platform/routes/scopes.d.ts.map +1 -0
  196. package/dist/apps/platform/routes/scopes.js +66 -0
  197. package/dist/apps/platform/routes/scopes.js.map +1 -0
  198. package/dist/apps/platform/routes/usage.d.ts +588 -0
  199. package/dist/apps/platform/routes/usage.d.ts.map +1 -0
  200. package/dist/apps/platform/routes/usage.js +391 -0
  201. package/dist/apps/platform/routes/usage.js.map +1 -0
  202. package/dist/apps/relay/App.d.ts +44 -0
  203. package/dist/apps/relay/App.d.ts.map +1 -0
  204. package/dist/apps/relay/App.js +211 -0
  205. package/dist/apps/relay/App.js.map +1 -0
  206. package/dist/apps/relay/Sponsorships.d.ts +107 -0
  207. package/dist/apps/relay/Sponsorships.d.ts.map +1 -0
  208. package/dist/apps/relay/Sponsorships.js +194 -0
  209. package/dist/apps/relay/Sponsorships.js.map +1 -0
  210. package/dist/apps/relay/index.d.ts +3 -0
  211. package/dist/apps/relay/index.d.ts.map +1 -0
  212. package/dist/apps/relay/index.js +3 -0
  213. package/dist/apps/relay/index.js.map +1 -0
  214. package/dist/cli/Cli.d.ts +9 -2
  215. package/dist/cli/Cli.d.ts.map +1 -1
  216. package/dist/cli/Cli.js +12 -4
  217. package/dist/cli/Cli.js.map +1 -1
  218. package/dist/cli/commands/admin.d.ts +5 -1
  219. package/dist/cli/commands/admin.d.ts.map +1 -1
  220. package/dist/cli/commands/admin.js +82 -3
  221. package/dist/cli/commands/admin.js.map +1 -1
  222. package/dist/db/Db.d.ts +17 -0
  223. package/dist/db/Db.d.ts.map +1 -1
  224. package/dist/db/Db.js +254 -0
  225. package/dist/db/Db.js.map +1 -1
  226. package/dist/db/Schema.d.ts +159 -0
  227. package/dist/db/Schema.d.ts.map +1 -1
  228. package/dist/db/Schema.js +187 -2
  229. package/dist/db/Schema.js.map +1 -1
  230. package/dist/db/tables/billingSettings.d.ts +55 -0
  231. package/dist/db/tables/billingSettings.d.ts.map +1 -0
  232. package/dist/db/tables/billingSettings.js +70 -0
  233. package/dist/db/tables/billingSettings.js.map +1 -0
  234. package/dist/db/tables/invitations.d.ts +84 -0
  235. package/dist/db/tables/invitations.d.ts.map +1 -0
  236. package/dist/db/tables/invitations.js +165 -0
  237. package/dist/db/tables/invitations.js.map +1 -0
  238. package/dist/db/tables/memberships.d.ts +108 -0
  239. package/dist/db/tables/memberships.d.ts.map +1 -0
  240. package/dist/db/tables/memberships.js +153 -0
  241. package/dist/db/tables/memberships.js.map +1 -0
  242. package/dist/db/tables/organizations.d.ts +48 -0
  243. package/dist/db/tables/organizations.d.ts.map +1 -1
  244. package/dist/db/tables/organizations.js +97 -2
  245. package/dist/db/tables/organizations.js.map +1 -1
  246. package/dist/db/tables/projects.d.ts +65 -0
  247. package/dist/db/tables/projects.d.ts.map +1 -0
  248. package/dist/db/tables/projects.js +75 -0
  249. package/dist/db/tables/projects.js.map +1 -0
  250. package/dist/db/tables/requestUsage.d.ts +121 -0
  251. package/dist/db/tables/requestUsage.d.ts.map +1 -0
  252. package/dist/db/tables/requestUsage.js +150 -0
  253. package/dist/db/tables/requestUsage.js.map +1 -0
  254. package/dist/db/tables/sponsoredTransactions.d.ts +239 -0
  255. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -0
  256. package/dist/db/tables/sponsoredTransactions.js +314 -0
  257. package/dist/db/tables/sponsoredTransactions.js.map +1 -0
  258. package/dist/db/tables/stripeCustomers.d.ts +81 -0
  259. package/dist/db/tables/stripeCustomers.d.ts.map +1 -0
  260. package/dist/db/tables/stripeCustomers.js +120 -0
  261. package/dist/db/tables/stripeCustomers.js.map +1 -0
  262. package/dist/db/tables/users.d.ts +48 -0
  263. package/dist/db/tables/users.d.ts.map +1 -0
  264. package/dist/db/tables/users.js +65 -0
  265. package/dist/db/tables/users.js.map +1 -0
  266. package/dist/handlers/internal/executionError.d.ts.map +1 -1
  267. package/dist/handlers/internal/executionError.js +2 -0
  268. package/dist/handlers/internal/executionError.js.map +1 -1
  269. package/dist/handlers/internal/multisig.d.ts +2 -6
  270. package/dist/handlers/internal/multisig.d.ts.map +1 -1
  271. package/dist/handlers/internal/multisig.js +11 -36
  272. package/dist/handlers/internal/multisig.js.map +1 -1
  273. package/dist/handlers/internal/sponsorship.d.ts +35 -2
  274. package/dist/handlers/internal/sponsorship.d.ts.map +1 -1
  275. package/dist/handlers/internal/sponsorship.js +53 -14
  276. package/dist/handlers/internal/sponsorship.js.map +1 -1
  277. package/dist/handlers/internal/utils.d.ts +10 -3
  278. package/dist/handlers/internal/utils.d.ts.map +1 -1
  279. package/dist/handlers/internal/utils.js +14 -6
  280. package/dist/handlers/internal/utils.js.map +1 -1
  281. package/dist/handlers/relay.d.ts +6 -2
  282. package/dist/handlers/relay.d.ts.map +1 -1
  283. package/dist/handlers/relay.js +16 -2
  284. package/dist/handlers/relay.js.map +1 -1
  285. package/dist/index.d.ts +1 -0
  286. package/dist/index.d.ts.map +1 -1
  287. package/dist/index.js +1 -0
  288. package/dist/index.js.map +1 -1
  289. package/dist/internal/Auth.d.ts +222 -6
  290. package/dist/internal/Auth.d.ts.map +1 -1
  291. package/dist/internal/Auth.js +625 -74
  292. package/dist/internal/Auth.js.map +1 -1
  293. package/dist/internal/Cache.d.ts.map +1 -1
  294. package/dist/internal/Cache.js +4 -1
  295. package/dist/internal/Cache.js.map +1 -1
  296. package/dist/internal/EdgeCache.d.ts +10 -0
  297. package/dist/internal/EdgeCache.d.ts.map +1 -1
  298. package/dist/internal/EdgeCache.js +39 -8
  299. package/dist/internal/EdgeCache.js.map +1 -1
  300. package/dist/internal/Fees.d.ts +27 -0
  301. package/dist/internal/Fees.d.ts.map +1 -0
  302. package/dist/internal/Fees.js +48 -0
  303. package/dist/internal/Fees.js.map +1 -0
  304. package/dist/internal/FundingRoutes.d.ts +183 -0
  305. package/dist/internal/FundingRoutes.d.ts.map +1 -0
  306. package/dist/internal/FundingRoutes.js +490 -0
  307. package/dist/internal/FundingRoutes.js.map +1 -0
  308. package/dist/internal/Id.d.ts +15 -0
  309. package/dist/internal/Id.d.ts.map +1 -0
  310. package/dist/internal/Id.js +22 -0
  311. package/dist/internal/Id.js.map +1 -0
  312. package/dist/internal/Log.d.ts +9 -3
  313. package/dist/internal/Log.d.ts.map +1 -1
  314. package/dist/internal/Log.js +7 -0
  315. package/dist/internal/Log.js.map +1 -1
  316. package/dist/internal/OpenApi.d.ts +10 -20
  317. package/dist/internal/OpenApi.d.ts.map +1 -1
  318. package/dist/internal/OpenApi.js +8 -58
  319. package/dist/internal/OpenApi.js.map +1 -1
  320. package/dist/internal/RateLimit.d.ts +25 -0
  321. package/dist/internal/RateLimit.d.ts.map +1 -1
  322. package/dist/internal/RateLimit.js +51 -0
  323. package/dist/internal/RateLimit.js.map +1 -1
  324. package/dist/internal/Response.d.ts +1 -1
  325. package/dist/internal/Response.d.ts.map +1 -1
  326. package/dist/internal/Schema.d.ts +8 -0
  327. package/dist/internal/Schema.d.ts.map +1 -1
  328. package/dist/internal/Schema.js +7 -0
  329. package/dist/internal/Schema.js.map +1 -1
  330. package/dist/internal/Store.d.ts +7 -1
  331. package/dist/internal/Store.d.ts.map +1 -1
  332. package/dist/internal/Store.js +16 -2
  333. package/dist/internal/Store.js.map +1 -1
  334. package/dist/internal/Viem.d.ts +84 -0
  335. package/dist/internal/Viem.d.ts.map +1 -1
  336. package/dist/internal/Webhooks.d.ts +1 -1
  337. package/dist/internal/Webhooks.d.ts.map +1 -1
  338. package/dist/internal/Webhooks.js +3 -3
  339. package/dist/internal/Webhooks.js.map +1 -1
  340. package/package.json +33 -6
  341. package/src/ApiKey.ts +10 -2
  342. package/src/ApiKeys.test.ts +155 -1
  343. package/src/ApiKeys.ts +203 -1
  344. package/src/App.test.ts +64 -75741
  345. package/src/App.ts +218 -420
  346. package/src/Client.test-d.ts +657 -15
  347. package/src/Scope.ts +26 -4
  348. package/src/Webhooks.ts +2 -2
  349. package/src/admin/apps/api-keys.test.ts +51 -0
  350. package/src/admin/apps/api-keys.ts +84 -0
  351. package/src/admin/apps/scopes.test.ts +11 -1
  352. package/src/admin/apps/scopes.ts +3 -0
  353. package/src/admin/apps/verified-tokens.ts +1 -1
  354. package/src/admin/ui/src/components/Page.tsx +2 -3
  355. package/src/admin/ui/src/routes/__root.tsx +2 -2
  356. package/src/admin/ui/src/routes/api-keys.tsx +2 -2
  357. package/src/admin/ui.generated.ts +13 -13
  358. package/src/analytics/Analytics.test.ts +527 -0
  359. package/src/analytics/Analytics.ts +318 -0
  360. package/src/analytics/tables/requestEvents.ts +523 -0
  361. package/src/apps/composition.test.ts +66 -0
  362. package/src/apps/data/App.test.ts +72 -0
  363. package/src/apps/data/App.ts +330 -0
  364. package/src/apps/data/index.ts +2 -0
  365. package/src/apps/{activities.test.ts → data/routes/activities.test.ts} +3 -3
  366. package/src/apps/{activities.ts → data/routes/activities.ts} +15 -15
  367. package/src/apps/{addresses.ts → data/routes/addresses.ts} +6 -6
  368. package/src/apps/{balances.test.ts → data/routes/balances.test.ts} +2 -2
  369. package/src/apps/{balances.ts → data/routes/balances.ts} +46 -12
  370. package/src/apps/{blocks.test.ts → data/routes/blocks.test.ts} +3 -3
  371. package/src/apps/{blocks.ts → data/routes/blocks.ts} +14 -14
  372. package/src/apps/{coingecko.test.ts → data/routes/coingecko.test.ts} +2 -2
  373. package/src/apps/{coingecko.ts → data/routes/coingecko.ts} +14 -14
  374. package/src/apps/{exchanges.test.ts → data/routes/exchanges.test.ts} +1 -1
  375. package/src/apps/{exchanges.ts → data/routes/exchanges.ts} +20 -20
  376. package/src/apps/{fee-amm.test.ts → data/routes/fee-amm.test.ts} +3 -3
  377. package/src/apps/{fee-amm.ts → data/routes/fee-amm.ts} +18 -18
  378. package/src/apps/data/routes/funding.test.ts +214 -0
  379. package/src/apps/data/routes/funding.ts +584 -0
  380. package/src/apps/{indexer.test.ts → data/routes/indexer.test.ts} +26 -5
  381. package/src/apps/{indexer.ts → data/routes/indexer.ts} +13 -10
  382. package/src/apps/{receipts.test.ts → data/routes/receipts.test.ts} +3 -3
  383. package/src/apps/{receipts.ts → data/routes/receipts.ts} +15 -22
  384. package/src/apps/{rpc.test.ts → data/routes/rpc.test.ts} +122 -7
  385. package/src/apps/{rpc.ts → data/routes/rpc.ts} +38 -8
  386. package/src/apps/{tokenlist.test.ts → data/routes/tokenlist.test.ts} +3 -3
  387. package/src/apps/{tokenlist.ts → data/routes/tokenlist.ts} +9 -9
  388. package/src/apps/{tokens.test.ts → data/routes/tokens.test.ts} +2 -2
  389. package/src/apps/{tokens.ts → data/routes/tokens.ts} +20 -20
  390. package/src/apps/{transactions.test.ts → data/routes/transactions.test.ts} +3 -3
  391. package/src/apps/{transactions.ts → data/routes/transactions.ts} +15 -15
  392. package/src/apps/{transfers.test.ts → data/routes/transfers.test.ts} +2 -2
  393. package/src/apps/{transfers.ts → data/routes/transfers.ts} +15 -15
  394. package/src/apps/{verified-tokens.test.ts → data/routes/verified-tokens.test.ts} +3 -3
  395. package/src/apps/{verified-tokens.ts → data/routes/verified-tokens.ts} +10 -10
  396. package/src/apps/{webhooks.test.ts → data/routes/webhooks.test.ts} +7 -7
  397. package/src/apps/{webhooks.ts → data/routes/webhooks.ts} +50 -43
  398. package/src/apps/docs/App.ts +65 -0
  399. package/src/apps/docs/index.ts +1 -0
  400. package/src/apps/docs/reference.ts +132 -0
  401. package/src/apps/index.ts +4 -0
  402. package/src/apps/mcp.test.ts +206 -0
  403. package/src/apps/mcp.ts +250 -0
  404. package/src/apps/metadata.ts +23 -0
  405. package/src/apps/platform/App.ts +169 -0
  406. package/src/apps/platform/Billing.test.ts +530 -0
  407. package/src/apps/platform/Billing.ts +682 -0
  408. package/src/apps/platform/index.ts +2 -0
  409. package/src/apps/platform/routes/api-keys.test.ts +493 -0
  410. package/src/apps/platform/routes/api-keys.ts +543 -0
  411. package/src/apps/platform/routes/billing.test.ts +1100 -0
  412. package/src/apps/platform/routes/billing.ts +966 -0
  413. package/src/apps/platform/routes/invitations.test.ts +409 -0
  414. package/src/apps/platform/routes/invitations.ts +587 -0
  415. package/src/apps/platform/routes/me.test.ts +351 -0
  416. package/src/apps/platform/routes/me.ts +106 -0
  417. package/src/apps/platform/routes/members.test.ts +195 -0
  418. package/src/apps/platform/routes/members.ts +313 -0
  419. package/src/apps/platform/routes/orgs.test.ts +313 -0
  420. package/src/apps/platform/routes/orgs.ts +365 -0
  421. package/src/apps/platform/routes/projects.test.ts +181 -0
  422. package/src/apps/platform/routes/projects.ts +367 -0
  423. package/src/apps/platform/routes/scopes.test.ts +29 -0
  424. package/src/apps/platform/routes/scopes.ts +85 -0
  425. package/src/apps/platform/routes/usage.test.ts +422 -0
  426. package/src/apps/platform/routes/usage.ts +527 -0
  427. package/src/apps/relay/App.test.ts +933 -0
  428. package/src/apps/relay/App.ts +236 -0
  429. package/src/apps/relay/Sponsorships.test.ts +375 -0
  430. package/src/apps/relay/Sponsorships.ts +306 -0
  431. package/src/apps/relay/index.ts +2 -0
  432. package/src/cli/Cli.test.ts +25 -2
  433. package/src/cli/Cli.ts +18 -3
  434. package/src/cli/commands/admin.test.ts +83 -0
  435. package/src/cli/commands/admin.ts +91 -3
  436. package/src/db/Db.ts +296 -0
  437. package/src/db/Schema.ts +241 -2
  438. package/src/db/tables/billingSettings.test.ts +87 -0
  439. package/src/db/tables/billingSettings.ts +108 -0
  440. package/src/db/tables/invitations.ts +198 -0
  441. package/src/db/tables/memberships.ts +208 -0
  442. package/src/db/tables/organizations.test.ts +38 -2
  443. package/src/db/tables/organizations.ts +117 -2
  444. package/src/db/tables/projects.ts +107 -0
  445. package/src/db/tables/requestUsage.ts +231 -0
  446. package/src/db/tables/sponsoredTransactions.test.ts +325 -0
  447. package/src/db/tables/sponsoredTransactions.ts +464 -0
  448. package/src/db/tables/stripeCustomers.test.ts +109 -0
  449. package/src/db/tables/stripeCustomers.ts +162 -0
  450. package/src/db/tables/users.ts +85 -0
  451. package/src/handlers/internal/executionError.ts +2 -0
  452. package/src/handlers/internal/multisig.test.ts +84 -10
  453. package/src/handlers/internal/multisig.ts +15 -50
  454. package/src/handlers/internal/sponsorship.ts +106 -20
  455. package/src/handlers/internal/utils.ts +19 -7
  456. package/src/handlers/{internal/relay.localnet.test.ts → relay.test.ts} +2 -2
  457. package/src/handlers/relay.ts +23 -5
  458. package/src/index.ts +1 -0
  459. package/src/internal/Auth.test.ts +491 -25
  460. package/src/internal/Auth.ts +839 -84
  461. package/src/internal/Cache.ts +5 -1
  462. package/src/internal/Cursor.test.ts +3 -2
  463. package/src/internal/EdgeCache.test.ts +27 -4
  464. package/src/internal/EdgeCache.ts +47 -9
  465. package/src/internal/Fees.test.ts +32 -0
  466. package/src/internal/Fees.ts +51 -0
  467. package/src/internal/FundingRoutes.ts +583 -0
  468. package/src/internal/Id.test.ts +17 -0
  469. package/src/internal/Id.ts +24 -0
  470. package/src/internal/Log.test.ts +2 -1
  471. package/src/internal/Log.ts +16 -3
  472. package/src/internal/MetricSink.ts +1 -1
  473. package/src/internal/OpenApi.ts +12 -60
  474. package/src/internal/RateLimit.test.ts +84 -0
  475. package/src/internal/RateLimit.ts +70 -0
  476. package/src/internal/Response.ts +1 -1
  477. package/src/internal/Schema.ts +8 -0
  478. package/src/internal/Store.test.ts +52 -0
  479. package/src/internal/Store.ts +24 -5
  480. package/src/internal/Tidx.local.test.ts +1 -1
  481. package/src/internal/VerifiedTokens.test.ts +1 -1
  482. package/src/internal/WebhookDestination.test.ts +1 -1
  483. package/src/internal/Webhooks.test.ts +7 -7
  484. package/src/internal/Webhooks.ts +4 -4
  485. package/dist/apps/activities.js.map +0 -1
  486. package/dist/apps/addresses.js.map +0 -1
  487. package/dist/apps/balances.d.ts.map +0 -1
  488. package/dist/apps/balances.js.map +0 -1
  489. package/dist/apps/blocks.d.ts.map +0 -1
  490. package/dist/apps/blocks.js.map +0 -1
  491. package/dist/apps/coingecko.d.ts.map +0 -1
  492. package/dist/apps/coingecko.js.map +0 -1
  493. package/dist/apps/exchanges.d.ts.map +0 -1
  494. package/dist/apps/exchanges.js.map +0 -1
  495. package/dist/apps/fee-amm.d.ts.map +0 -1
  496. package/dist/apps/fee-amm.js.map +0 -1
  497. package/dist/apps/indexer.d.ts.map +0 -1
  498. package/dist/apps/indexer.js.map +0 -1
  499. package/dist/apps/receipts.d.ts.map +0 -1
  500. package/dist/apps/receipts.js.map +0 -1
  501. package/dist/apps/rpc.d.ts.map +0 -1
  502. package/dist/apps/rpc.js.map +0 -1
  503. package/dist/apps/tokenlist.d.ts.map +0 -1
  504. package/dist/apps/tokenlist.js.map +0 -1
  505. package/dist/apps/tokens.d.ts.map +0 -1
  506. package/dist/apps/tokens.js.map +0 -1
  507. package/dist/apps/transactions.d.ts.map +0 -1
  508. package/dist/apps/transactions.js.map +0 -1
  509. package/dist/apps/transfers.d.ts.map +0 -1
  510. package/dist/apps/transfers.js.map +0 -1
  511. package/dist/apps/verified-tokens.d.ts.map +0 -1
  512. package/dist/apps/verified-tokens.js.map +0 -1
  513. package/dist/apps/webhooks.js.map +0 -1
  514. package/dist/internal/OpenApi.fonts.d.ts.map +0 -1
  515. package/dist/internal/OpenApi.fonts.js.map +0 -1
  516. package/dist/internal/OpenApi.logo.d.ts.map +0 -1
  517. package/dist/internal/OpenApi.logo.js.map +0 -1
  518. package/src/handlers/internal/multisig.localnet.test.ts +0 -146
  519. /package/src/{internal/OpenApi.fonts.ts → apps/docs/fonts.ts} +0 -0
  520. /package/src/{internal/OpenApi.logo.ts → apps/docs/logo.ts} +0 -0
@@ -0,0 +1,933 @@
1
+ import { nanoid } from 'nanoid'
2
+ import { Hash, Secp256k1 } from 'ox'
3
+ import { createClient, http } from 'viem'
4
+ import { prepareTransactionRequest, sendRawTransactionSync, signTransaction } from 'viem/actions'
5
+ import { Account as TempoAccount, Actions, Transaction, withRelay } from 'viem/tempo'
6
+
7
+ import * as TestApp from '../../../test/App.js'
8
+ import { startTempo } from '../../../test/containers.js'
9
+ import * as Relay from '../../../test/Relay.js'
10
+ import * as Runtime from '../../../test/runtime.js'
11
+ import * as Tempo from '../../../test/Tempo.js'
12
+ import * as TestStripe from '../../../test/Stripe.js'
13
+ import * as BillingSettings from '../../db/tables/billingSettings.js'
14
+ import type * as Db from '../../db/Db.js'
15
+ import * as Fees from '../../internal/Fees.js'
16
+ import * as Organizations from '../../db/tables/organizations.js'
17
+ import * as SponsoredTransactions from '../../db/tables/sponsoredTransactions.js'
18
+ import * as StripeCustomers from '../../db/tables/stripeCustomers.js'
19
+ import * as core_Billing from '../platform/Billing.js'
20
+ import * as RequestListener from '../../handlers/internal/requestListener.js'
21
+ import * as Viem from '../../internal/Viem.js'
22
+ import * as Sponsorships from './Sponsorships.js'
23
+
24
+ const runtime = Runtime.get()
25
+
26
+ const feePayerAccount = Tempo.accounts[10]!
27
+ const senderAccount = Tempo.accounts[9]!
28
+
29
+ /** Super admin secret configured on the end-to-end billing app. */
30
+ const superAdminSecret = 'tempo:sk:a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1'
31
+
32
+ /** Keys covering the sponsorship policy matrix; all hold the base relay scope. */
33
+ const keys = [
34
+ {
35
+ id: 'key_sponsor',
36
+ orgId: 'org_1',
37
+ projectId: 'prj_1',
38
+ environment: 'sandbox',
39
+ scopes: ['rpc-relay:read', 'rpc-relay:sponsor'],
40
+ token: 'secret_sponsor',
41
+ },
42
+ {
43
+ id: 'key_sponsor_prod',
44
+ orgId: 'org_1',
45
+ projectId: 'prj_1',
46
+ environment: 'production',
47
+ scopes: ['rpc-relay:read', 'rpc-relay:sponsor'],
48
+ token: 'secret_sponsor_prod',
49
+ },
50
+ {
51
+ id: 'key_wildcard',
52
+ orgId: 'org_2',
53
+ projectId: 'prj_2',
54
+ environment: 'sandbox',
55
+ scopes: ['*'],
56
+ token: 'secret_wildcard',
57
+ },
58
+ {
59
+ id: 'key_wildcard_prod',
60
+ orgId: 'org_2',
61
+ projectId: 'prj_2',
62
+ environment: 'production',
63
+ scopes: ['*'],
64
+ token: 'secret_wildcard_prod',
65
+ },
66
+ {
67
+ id: 'key_unscoped',
68
+ orgId: 'org_1',
69
+ projectId: 'prj_1',
70
+ environment: 'sandbox',
71
+ scopes: ['rpc-relay:read'],
72
+ token: 'secret_unscoped',
73
+ },
74
+ {
75
+ id: 'key_unattributed',
76
+ orgId: 'org_1',
77
+ environment: 'sandbox',
78
+ scopes: ['rpc-relay:read', 'rpc-relay:sponsor'],
79
+ token: 'secret_unattributed',
80
+ },
81
+ ] as const satisfies readonly TestApp.kvStore.Key[]
82
+
83
+ describe.skipIf(runtime.mode !== 'localnet')('behavior: sponsorship', () => {
84
+ test('behavior: scoped keys sponsor, recorded with attribution', async () => {
85
+ const db = TestApp.database()
86
+ const app = TestApp.create({
87
+ auth: { keys },
88
+ db,
89
+ relay: { feePayer: { account: feePayerAccount } },
90
+ })
91
+
92
+ const body = await relayRequest(app, {
93
+ method: 'eth_signRawTransaction',
94
+ params: [await signSponsorable()],
95
+ token: 'secret_sponsor',
96
+ })
97
+ expect(body.error).toBeUndefined()
98
+ // The relay returned the envelope counter-signed by the fee payer.
99
+ const signed = Transaction.deserialize(body.result as `0x76${string}`)
100
+ expect(signed.feePayerSignature).toBeDefined()
101
+
102
+ const [row] = await SponsoredTransactions.listPending(db)
103
+ expect(row?.transactionHash).toBe(Hash.keccak256(body.result!))
104
+ expect(row?.signPayload).toMatch(/^0x/)
105
+ expect(row?.chainId).toBe(runtime.chainId)
106
+ // The stored envelope is exactly the counter-signed transaction returned.
107
+ expect(row?.transaction).toBe(body.result)
108
+ expect({ ...row, chainId: '<chainId>', createdAt: '<timestamp>', id: '<id>', signPayload: '<hash>', transaction: '<hex>', transactionHash: '<hash>' }) // prettier-ignore
109
+ .toMatchInlineSnapshot(`
110
+ {
111
+ "apiKeyId": "key_sponsor",
112
+ "billable": false,
113
+ "chainId": "<chainId>",
114
+ "createdAt": "<timestamp>",
115
+ "currency": null,
116
+ "environment": "sandbox",
117
+ "feeAmount": null,
118
+ "feeMax": "1",
119
+ "feeToken": "0x20c0000000000000000000000000000000000000",
120
+ "finalizedAt": null,
121
+ "id": "<id>",
122
+ "meterReportedAt": null,
123
+ "orgId": "org_1",
124
+ "projectId": "prj_1",
125
+ "signPayload": "<hash>",
126
+ "status": "pending",
127
+ "transaction": "<hex>",
128
+ "transactionHash": "<hash>",
129
+ }
130
+ `)
131
+ })
132
+
133
+ test('behavior: production keys record billable rows', async () => {
134
+ const db = TestApp.database()
135
+ const app = TestApp.create({
136
+ auth: { keys },
137
+ db,
138
+ relay: { feePayer: { account: feePayerAccount } },
139
+ })
140
+
141
+ const body = await relayRequest(app, {
142
+ method: 'eth_signRawTransaction',
143
+ params: [await signSponsorable()],
144
+ token: 'secret_sponsor_prod',
145
+ })
146
+ expect(body.error).toBeUndefined()
147
+
148
+ const [row] = await SponsoredTransactions.listPending(db)
149
+ expect(row?.billable).toBe(true)
150
+ expect(row?.environment).toMatchInlineSnapshot(`"production"`)
151
+ })
152
+
153
+ test('behavior: the wildcard scope grants sponsorship', async () => {
154
+ const db = TestApp.database()
155
+ const app = TestApp.create({
156
+ auth: { keys },
157
+ db,
158
+ relay: { feePayer: { account: feePayerAccount } },
159
+ })
160
+
161
+ const body = await relayRequest(app, {
162
+ method: 'eth_signRawTransaction',
163
+ params: [await signSponsorable()],
164
+ token: 'secret_wildcard',
165
+ })
166
+ expect(body.error).toBeUndefined()
167
+
168
+ const [row] = await SponsoredTransactions.listPending(db)
169
+ expect(row?.orgId).toMatchInlineSnapshot(`"org_2"`)
170
+ })
171
+
172
+ test('behavior: keys without the sponsor scope are refused', async () => {
173
+ const db = TestApp.database()
174
+ const app = TestApp.create({
175
+ auth: { keys },
176
+ db,
177
+ relay: { feePayer: { account: feePayerAccount } },
178
+ })
179
+
180
+ const body = await relayRequest(app, {
181
+ method: 'eth_signRawTransaction',
182
+ params: [await signSponsorable()],
183
+ token: 'secret_unscoped',
184
+ })
185
+ // "Sponsorship rejected." surfaces as invalid params (-32602).
186
+ expect(body.result).toBeUndefined()
187
+ expect(body.error?.code).toBe(-32602)
188
+ expect(await SponsoredTransactions.listPending(db)).toHaveLength(0)
189
+ })
190
+
191
+ test('behavior: keys without project attribution are refused', async () => {
192
+ const db = TestApp.database()
193
+ const app = TestApp.create({
194
+ auth: { keys },
195
+ db,
196
+ relay: { feePayer: { account: feePayerAccount } },
197
+ })
198
+
199
+ const body = await relayRequest(app, {
200
+ method: 'eth_signRawTransaction',
201
+ params: [await signSponsorable()],
202
+ token: 'secret_unattributed',
203
+ })
204
+ expect(body.result).toBeUndefined()
205
+ expect(body.error?.code).toBe(-32602)
206
+ expect(await SponsoredTransactions.listPending(db)).toHaveLength(0)
207
+ })
208
+
209
+ test('behavior: super admin principals are refused', async () => {
210
+ const db = TestApp.database()
211
+ const app = TestApp.create({
212
+ auth: { keys, superAdmin: { secret: 'super-admin-secret' } },
213
+ db,
214
+ relay: { feePayer: { account: feePayerAccount } },
215
+ })
216
+
217
+ const body = await relayRequest(app, {
218
+ method: 'eth_signRawTransaction',
219
+ params: [await signSponsorable()],
220
+ token: 'super-admin-secret',
221
+ })
222
+ expect(body.result).toBeUndefined()
223
+ expect(body.error?.code).toBe(-32602)
224
+ expect(await SponsoredTransactions.listPending(db)).toHaveLength(0)
225
+ })
226
+
227
+ test('behavior: sponsorship stays closed without a fee payer', async () => {
228
+ const db = TestApp.database()
229
+ const app = TestApp.create({ auth: { keys }, db })
230
+
231
+ const body = await relayRequest(app, {
232
+ method: 'eth_signRawTransaction',
233
+ params: [await signSponsorable()],
234
+ token: 'secret_sponsor',
235
+ })
236
+ expect(body.error?.code).toBe(-32601)
237
+ expect(await SponsoredTransactions.listPending(db)).toHaveLength(0)
238
+ })
239
+
240
+ test('behavior: mainnet sponsorship is refused until billing ships', async () => {
241
+ const db = TestApp.database()
242
+ const app = TestApp.create({
243
+ auth: { keys },
244
+ db,
245
+ relay: { feePayer: { account: feePayerAccount } },
246
+ })
247
+
248
+ const body = await relayRequest(app, {
249
+ method: 'eth_signRawTransaction',
250
+ params: [
251
+ await senderAccount.signTransaction({
252
+ chainId: 4217,
253
+ feePayer: true,
254
+ gas: 100_000n,
255
+ maxFeePerGas: 1_000_000n,
256
+ maxPriorityFeePerGas: 0n,
257
+ nonce: 0,
258
+ to: Tempo.accounts[8]!.address,
259
+ value: 0n,
260
+ }),
261
+ ],
262
+ token: 'secret_sponsor',
263
+ })
264
+ expect(body.result).toBeUndefined()
265
+ expect(body.error?.code).toBe(-32602)
266
+ expect(await SponsoredTransactions.listPending(db)).toHaveLength(0)
267
+ })
268
+
269
+ test('behavior: an app-provided fee payer URL cannot trigger the managed fee payer', async () => {
270
+ const db = TestApp.database()
271
+ const app = TestApp.create({
272
+ auth: { keys },
273
+ db,
274
+ relay: { feePayer: { account: feePayerAccount } },
275
+ })
276
+
277
+ const body = await relayRequest(app, {
278
+ method: 'eth_fillTransaction',
279
+ params: [
280
+ {
281
+ calls: [{ to: Tempo.accounts[8]!.address, value: '0x0' }],
282
+ chainId: `0x${runtime.chainId.toString(16)}`,
283
+ feePayer: 'https://example.com',
284
+ from: senderAccount.address,
285
+ gas: '0x186a0',
286
+ maxFeePerGas: '0xf4240',
287
+ maxPriorityFeePerGas: '0x0',
288
+ nonce: '0x0',
289
+ },
290
+ ],
291
+ token: 'secret_unscoped',
292
+ })
293
+ const result = body.result as unknown as
294
+ | { capabilities?: { sponsored?: boolean }; tx?: { feePayerSignature?: string | null } }
295
+ | undefined
296
+ expect(result?.tx?.feePayerSignature ?? null).toBeNull()
297
+ expect(result?.capabilities?.sponsored).toBe(false)
298
+ expect(await SponsoredTransactions.listPending(db)).toHaveLength(0)
299
+ })
300
+
301
+ test('behavior: sponsorship is refused for chains this deployment does not serve', async () => {
302
+ const db = TestApp.database()
303
+ const app = TestApp.create({
304
+ auth: { keys },
305
+ db,
306
+ relay: { feePayer: { account: feePayerAccount } },
307
+ })
308
+
309
+ const body = await relayRequest(app, {
310
+ method: 'eth_signRawTransaction',
311
+ params: [
312
+ await senderAccount.signTransaction({
313
+ chainId: 999_999,
314
+ feePayer: true,
315
+ gas: 100_000n,
316
+ maxFeePerGas: 1_000_000n,
317
+ maxPriorityFeePerGas: 0n,
318
+ nonce: 0,
319
+ to: Tempo.accounts[8]!.address,
320
+ value: 0n,
321
+ }),
322
+ ],
323
+ token: 'secret_sponsor',
324
+ })
325
+ expect(body.result).toBeUndefined()
326
+ expect(body.error?.code).toBe(-32602)
327
+ expect(await SponsoredTransactions.listPending(db)).toHaveLength(0)
328
+ })
329
+ })
330
+
331
+ // Chain-free (`eth_signRawTransaction` signs locally; the sponsor fee token
332
+ // short-circuits resolution), so the gate matrix runs in every mode.
333
+ describe('behavior: mainnet billing gate', () => {
334
+ function createApp(db: Db.Source = TestApp.database()) {
335
+ return TestApp.create({
336
+ auth: { keys },
337
+ db,
338
+ relay: { feePayer: { account: feePayerAccount } },
339
+ supportedChainIds: [Viem.chainId.mainnet],
340
+ })
341
+ }
342
+
343
+ /** Activates billing for the org so the gate reaches the limit checks. */
344
+ async function activate(db: ReturnType<typeof TestApp.database>, orgId = 'org_1') {
345
+ await StripeCustomers.create(db, { orgId, stripeCustomerId: `cus_gate_${orgId}` })
346
+ await StripeCustomers.setStatus(db, orgId, 'active')
347
+ // The fee-token gate reads the verified list; seed mainnet's curated tokens.
348
+ await TestApp.verifiedSeed(db, Viem.chainId.mainnet)
349
+ }
350
+
351
+ // Gas prices are attodollars (1e-18 USD) per gas; the signed cap in
352
+ // fee-token units is ceil(gas × maxFeePerGas / 1e12). The helper's default
353
+ // envelope (100k gas × 1e6 atto) caps at 1 microdollar — negligible.
354
+ /** A $0.50 signed cap: 1M gas × 5e11 attodollars. */
355
+ const halfDollarFees = { gas: 1_000_000n, maxFeePerGas: 500_000_000_000n } as const
356
+ /** A $2 signed cap, over the `'1.00'` platform default. */
357
+ const overCapFees = { gas: 1_000_000n, maxFeePerGas: 2_000_000_000_000n } as const
358
+
359
+ test('behavior: refuses mainnet sponsorship without a billing source', async () => {
360
+ const body = await relayRequest(createApp(), {
361
+ method: 'eth_signRawTransaction',
362
+ params: [await signSponsorable({ chainId: Viem.chainId.mainnet })],
363
+ token: 'secret_sponsor_prod',
364
+ })
365
+ expect(body.error?.code).toBe(-32602)
366
+ expect(body.error?.message).toBe('Billing required.')
367
+ expect(body.error?.data).toStrictEqual({ code: 'billing_required' })
368
+ })
369
+
370
+ test('behavior: refuses mainnet sponsorship while billing is inactive', async () => {
371
+ const db = TestApp.database()
372
+ await StripeCustomers.create(db, { orgId: 'org_1', stripeCustomerId: 'cus_gate' })
373
+ const body = await relayRequest(createApp(db), {
374
+ method: 'eth_signRawTransaction',
375
+ params: [await signSponsorable({ chainId: Viem.chainId.mainnet })],
376
+ token: 'secret_sponsor_prod',
377
+ })
378
+ expect(body.error?.code).toBe(-32602)
379
+ expect(body.error?.message).toBe('Billing required.')
380
+ })
381
+
382
+ test('behavior: past-due billing refuses with its own reason', async () => {
383
+ const db = TestApp.database()
384
+ await activate(db)
385
+ // An unpaid invoice parked the org; the caller must settle it, not attach a card.
386
+ await StripeCustomers.setStatus(db, 'org_1', 'past_due')
387
+ const body = await relayRequest(createApp(db), {
388
+ method: 'eth_signRawTransaction',
389
+ params: [await signSponsorable({ chainId: Viem.chainId.mainnet, ...halfDollarFees })],
390
+ token: 'secret_sponsor_prod',
391
+ })
392
+ expect(body.error?.code).toBe(-32602)
393
+ expect(body.error?.message).toBe('Billing past due.')
394
+ expect(body.error?.data).toStrictEqual({ code: 'billing_past_due' })
395
+ })
396
+
397
+ test('behavior: active billing opens mainnet sponsorship and records it', async () => {
398
+ const db = TestApp.database()
399
+ await activate(db)
400
+
401
+ const body = await relayRequest(createApp(db), {
402
+ method: 'eth_signRawTransaction',
403
+ params: [await signSponsorable({ chainId: Viem.chainId.mainnet, ...halfDollarFees })],
404
+ token: 'secret_sponsor_prod',
405
+ })
406
+ expect(body.error).toBeUndefined()
407
+ const signed = Transaction.deserialize(body.result as `0x76${string}`)
408
+ expect(signed.feePayerSignature).toBeDefined()
409
+
410
+ const [row] = await SponsoredTransactions.listPending(db)
411
+ expect(row?.chainId).toBe(Viem.chainId.mainnet)
412
+ expect(row?.billable).toBe(true)
413
+ expect(row?.orgId).toBe('org_1')
414
+ // The signed liability cap in fee-token units, for spend-limit accounting.
415
+ expect(row?.feeMax).toBe('500000')
416
+ })
417
+
418
+ test('behavior: a sandbox key never sponsors mainnet, even with active billing', async () => {
419
+ const db = TestApp.database()
420
+ await activate(db) // org_1 is active…
421
+ const body = await relayRequest(createApp(db), {
422
+ method: 'eth_signRawTransaction',
423
+ params: [await signSponsorable({ chainId: Viem.chainId.mainnet, ...halfDollarFees })],
424
+ token: 'secret_sponsor', // …but this key is sandbox.
425
+ })
426
+ // Refused for the environment, before the billing check reveals a reason.
427
+ expect(body.error?.code).toBe(-32602)
428
+ expect(body.error?.message).toBe('Sponsorship rejected.')
429
+ expect(await SponsoredTransactions.listPending(db)).toHaveLength(0)
430
+ })
431
+
432
+ test('behavior: other orgs stay gated by their own billing', async () => {
433
+ const db = TestApp.database()
434
+ // org_1 is active; the production wildcard key belongs to org_2, which has nothing.
435
+ await activate(db)
436
+ const body = await relayRequest(createApp(db), {
437
+ method: 'eth_signRawTransaction',
438
+ params: [await signSponsorable({ chainId: Viem.chainId.mainnet, ...halfDollarFees })],
439
+ token: 'secret_wildcard_prod',
440
+ })
441
+ expect(body.error?.code).toBe(-32602)
442
+ expect(body.error?.message).toBe('Billing required.')
443
+ })
444
+
445
+ test('behavior: refuses a transaction over the platform fee cap', async () => {
446
+ const db = TestApp.database()
447
+ await activate(db)
448
+ const body = await relayRequest(createApp(db), {
449
+ method: 'eth_signRawTransaction',
450
+ params: [await signSponsorable({ chainId: Viem.chainId.mainnet, ...overCapFees })],
451
+ token: 'secret_sponsor_prod',
452
+ })
453
+ expect(body.error?.code).toBe(-32602)
454
+ expect(body.error?.message).toBe('Transaction fee limit exceeded.')
455
+ expect(body.error?.data).toStrictEqual({ code: 'tx_fee_limit_exceeded' })
456
+ })
457
+
458
+ test('behavior: the org fee limit tightens the platform cap but never loosens it', async () => {
459
+ const db = TestApp.database()
460
+ await activate(db)
461
+ // Tighter than the platform default: $0.50 envelopes now refuse.
462
+ await BillingSettings.upsert(db, { orgId: 'org_1', txFeeLimit: '0.10' })
463
+ const app = createApp(db)
464
+ const tightened = await relayRequest(app, {
465
+ method: 'eth_signRawTransaction',
466
+ params: [await signSponsorable({ chainId: Viem.chainId.mainnet, ...halfDollarFees })],
467
+ token: 'secret_sponsor_prod',
468
+ })
469
+ expect(tightened.error?.data).toStrictEqual({ code: 'tx_fee_limit_exceeded' })
470
+
471
+ // Looser than the platform default: the $2 envelope still refuses.
472
+ await BillingSettings.upsert(db, { orgId: 'org_1', txFeeLimit: '999999' })
473
+ const loosened = await relayRequest(app, {
474
+ method: 'eth_signRawTransaction',
475
+ params: [await signSponsorable({ chainId: Viem.chainId.mainnet, ...overCapFees })],
476
+ token: 'secret_sponsor_prod',
477
+ })
478
+ expect(loosened.error?.data).toStrictEqual({ code: 'tx_fee_limit_exceeded' })
479
+ })
480
+
481
+ test('behavior: refuses fee tokens the verified list does not resolve to USD', async () => {
482
+ const db = TestApp.database()
483
+ await activate(db)
484
+ // EURC is verified yet non-USD; fee math reads micro-USD, so it refuses.
485
+ const eurc = `0x${'e0'.repeat(20)}` as const
486
+ await TestApp.verifiedSeed(db, Viem.chainId.mainnet, {
487
+ tokens: [{ address: eurc, currency: 'EUR', decimals: 6, name: 'Euro Coin', symbol: 'EURC' }],
488
+ })
489
+
490
+ const app = createApp(db)
491
+ const request = async (feeToken: `0x${string}`) =>
492
+ await relayRequest(app, {
493
+ method: 'eth_signRawTransaction',
494
+ params: [await signSponsorable({ chainId: Viem.chainId.mainnet, feeToken })],
495
+ token: 'secret_sponsor_prod',
496
+ })
497
+ const nonUsd = await request(eurc)
498
+ expect(nonUsd.error?.message).toBe('Fee token unsupported.')
499
+ expect(nonUsd.error?.data).toStrictEqual({ code: 'fee_token_unsupported' })
500
+ // Tokens absent from the verified list refuse the same way.
501
+ const unverified = await request(`0x${'ab'.repeat(20)}`)
502
+ expect(unverified.error?.data).toStrictEqual({ code: 'fee_token_unsupported' })
503
+ expect(await SponsoredTransactions.listPending(db)).toHaveLength(0)
504
+ })
505
+
506
+ test('behavior: sponsored rows snapshot the fee currency', async () => {
507
+ const db = TestApp.database()
508
+ await activate(db)
509
+
510
+ const body = await relayRequest(createApp(db), {
511
+ method: 'eth_signRawTransaction',
512
+ params: [await signSponsorable({ chainId: Viem.chainId.mainnet, ...halfDollarFees })],
513
+ token: 'secret_sponsor_prod',
514
+ })
515
+ expect(body.error).toBeUndefined()
516
+ const [row] = await SponsoredTransactions.listPending(db)
517
+ expect(row?.currency).toBe('usd')
518
+ })
519
+
520
+ test('behavior: refuses sponsorship over the period spend limit; raising restores', async () => {
521
+ const db = TestApp.database()
522
+ await activate(db)
523
+ await BillingSettings.upsert(db, { orgId: 'org_1', spendLimit: '1' })
524
+ // $0.90 already finalized this window; a $0.50 cap would overshoot $1.
525
+ const seeded = await SponsoredTransactions.upsert(db, {
526
+ apiKeyId: 'key_sponsor_prod',
527
+ billable: true,
528
+ chainId: Viem.chainId.mainnet,
529
+ environment: 'production',
530
+ orgId: 'org_1',
531
+ projectId: 'prj_1',
532
+ signPayload: `0x${'31'.repeat(32)}`,
533
+ transaction: `0x76${'cc'.repeat(16)}`,
534
+ transactionHash: `0x${'32'.repeat(32)}`,
535
+ })
536
+ await SponsoredTransactions.finalize(db, seeded.id, {
537
+ feeAmount: '900000',
538
+ finalizedAt: new Date().toISOString(),
539
+ })
540
+
541
+ const app = createApp(db)
542
+ const request = async () =>
543
+ await relayRequest(app, {
544
+ method: 'eth_signRawTransaction',
545
+ params: [await signSponsorable({ chainId: Viem.chainId.mainnet, ...halfDollarFees })],
546
+ token: 'secret_sponsor_prod',
547
+ })
548
+ const refused = await request()
549
+ expect(refused.error?.message).toBe('Spend limit exceeded.')
550
+ expect(refused.error?.data).toStrictEqual({ code: 'spend_limit_exceeded' })
551
+
552
+ await BillingSettings.upsert(db, { orgId: 'org_1', spendLimit: '10' })
553
+ const restored = await request()
554
+ expect(restored.error).toBeUndefined()
555
+ })
556
+
557
+ test('behavior: pending signed caps consume spend-limit budget', async () => {
558
+ const db = TestApp.database()
559
+ await activate(db)
560
+ await BillingSettings.upsert(db, { orgId: 'org_1', spendLimit: '1' })
561
+ // A pending $0.90 cap, fee unknown until its receipt lands.
562
+ await SponsoredTransactions.upsert(db, {
563
+ apiKeyId: 'key_sponsor_prod',
564
+ billable: true,
565
+ chainId: Viem.chainId.mainnet,
566
+ environment: 'production',
567
+ feeMax: '900000',
568
+ orgId: 'org_1',
569
+ projectId: 'prj_1',
570
+ signPayload: `0x${'33'.repeat(32)}`,
571
+ transaction: `0x76${'cc'.repeat(16)}`,
572
+ transactionHash: `0x${'34'.repeat(32)}`,
573
+ })
574
+
575
+ const body = await relayRequest(createApp(db), {
576
+ method: 'eth_signRawTransaction',
577
+ params: [await signSponsorable({ chainId: Viem.chainId.mainnet, ...halfDollarFees })],
578
+ token: 'secret_sponsor_prod',
579
+ })
580
+ expect(body.error?.data).toStrictEqual({ code: 'spend_limit_exceeded' })
581
+ })
582
+
583
+ test('behavior: concurrent bursts cannot overshoot the spend limit', async () => {
584
+ // A per-request factory (like production) over a multi-connection pool, so
585
+ // the reserve transactions run concurrently and the advisory lock — not the
586
+ // pool — is what serializes them.
587
+ const db = TestApp.databaseFactory()
588
+ const seed = db()
589
+ await activate(seed)
590
+ // Limit fits exactly two $0.50 sponsorships.
591
+ await BillingSettings.upsert(seed, { orgId: 'org_1', spendLimit: '1' })
592
+ const app = createApp(db)
593
+ // Distinct nonces → distinct envelopes → distinct rows, fired together.
594
+ const results = await Promise.all(
595
+ Array.from({ length: 6 }, async (_, nonce) =>
596
+ relayRequest(app, {
597
+ method: 'eth_signRawTransaction',
598
+ params: [await signSponsorable({ chainId: Viem.chainId.mainnet, nonce, ...halfDollarFees })], // prettier-ignore
599
+ token: 'secret_sponsor_prod',
600
+ }),
601
+ ),
602
+ )
603
+ const sponsored = results.filter((r) => !r.error)
604
+ const refused = results.filter((r) => r.error?.data?.code === 'spend_limit_exceeded')
605
+ expect(sponsored).toHaveLength(2)
606
+ expect(refused).toHaveLength(4)
607
+ // Recorded spend never exceeds the limit ($1 = 1e6 base units).
608
+ const spend = await SponsoredTransactions.spend(seed, {
609
+ chainIds: [Viem.chainId.mainnet],
610
+ orgId: 'org_1',
611
+ since: '2000-01-01T00:00:00.000Z',
612
+ })
613
+ expect(spend).toBe(1_000_000n)
614
+ })
615
+ })
616
+
617
+ describe.skipIf(runtime.mode !== 'localnet')('behavior: sponsorship (viem)', () => {
618
+ const alphaUsd = '0x20c0000000000000000000000000000000000001'
619
+ const feePayer = TempoAccount.fromSecp256k1(Secp256k1.randomPrivateKey())
620
+ const sender = TempoAccount.fromSecp256k1(Secp256k1.randomPrivateKey())
621
+ const relayClient = Relay.getClient()
622
+
623
+ let db: ReturnType<typeof TestApp.database>
624
+ let server: Relay.Server
625
+
626
+ function getClient(policy?: 'sign-and-broadcast' | 'sign-only') {
627
+ return createClient({
628
+ account: sender,
629
+ chain: Tempo.chain,
630
+ transport: withRelay(
631
+ Relay.http(),
632
+ http(`${server.url}/rpc/relay`, {
633
+ fetchOptions: { headers: { 'tempo-api-key': 'secret_sponsor' } },
634
+ }),
635
+ policy ? { policy } : {},
636
+ ),
637
+ })
638
+ }
639
+
640
+ beforeAll(async () => {
641
+ await Promise.all(
642
+ [feePayer, sender].map((account) =>
643
+ Actions.faucet.fundSync(relayClient, { account, timeout: 60_000 }),
644
+ ),
645
+ )
646
+ db = TestApp.database()
647
+ const app = TestApp.create({
648
+ auth: { keys },
649
+ db,
650
+ // Explicit fee token: exercises the consumer override of the pathUSD default.
651
+ relay: { feePayer: { account: feePayer, feeToken: alphaUsd } },
652
+ })
653
+ server = await Relay.createServer(RequestListener.fromFetchHandler((req) => app.fetch(req)))
654
+ }, 120_000)
655
+
656
+ afterAll(async () => {
657
+ await server.closeAsync()
658
+ })
659
+
660
+ test('behavior: transferSync sponsors end-to-end, recording a fill intent', async () => {
661
+ const { receipt } = await Actions.token.transferSync(getClient(), {
662
+ amount: 1n,
663
+ feePayer: true,
664
+ to: Tempo.accounts[8]!.address,
665
+ token: alphaUsd,
666
+ })
667
+
668
+ // Sponsored on-chain: the fee payer paid, the sender's transfer landed.
669
+ expect(receipt.status).toBe('success')
670
+ expect(receipt.feePayer?.toLowerCase()).toBe(feePayer.address.toLowerCase())
671
+
672
+ const intent = (await SponsoredTransactions.listPending(db)).find(
673
+ (row) => row.transactionHash === null,
674
+ )
675
+ expect(intent?.apiKeyId).toBe('key_sponsor')
676
+ expect(intent?.orgId).toBe('org_1')
677
+ expect(intent?.billable).toBe(false)
678
+ expect(intent?.feeToken?.toLowerCase()).toBe(alphaUsd)
679
+
680
+ const finalizer = Sponsorships.createFinalizer({
681
+ db,
682
+ defaultChainId: runtime.chainId,
683
+ feePayer: feePayer.address,
684
+ rpc: TestApp.rpc,
685
+ tidx: { baseUrl: runtime.tidxUrl! },
686
+ })
687
+ let finalized: Awaited<ReturnType<typeof SponsoredTransactions.get>>
688
+ for (let attempt = 0; attempt < 30; attempt++) {
689
+ await finalizer.tick()
690
+ finalized = await SponsoredTransactions.get(db, intent!.id)
691
+ if (finalized?.status === 'finalized') break
692
+ await new Promise((resolve) => setTimeout(resolve, 1_000))
693
+ }
694
+ expect(finalized?.status).toBe('finalized')
695
+ expect(finalized?.transactionHash).toBe(receipt.transactionHash)
696
+ expect(finalized?.feeAmount).toBe(
697
+ Fees.fromGas(receipt.gasUsed, receipt.effectiveGasPrice).toString(),
698
+ )
699
+ }, 60_000)
700
+
701
+ test('behavior: sign-only sends record and finalize from the real receipt', async () => {
702
+ const request = await prepareTransactionRequest(relayClient, {
703
+ account: sender,
704
+ calls: [
705
+ Actions.token.transfer.call(relayClient, {
706
+ amount: 1n,
707
+ to: Tempo.accounts[8]!.address,
708
+ token: alphaUsd,
709
+ }),
710
+ ],
711
+ feePayer: true,
712
+ })
713
+ const signed = await signTransaction(relayClient, request)
714
+
715
+ const receipt = await sendRawTransactionSync(getClient(), {
716
+ serializedTransaction: signed,
717
+ })
718
+
719
+ const row = (await SponsoredTransactions.listPending(db)).find(
720
+ (row) => row.transactionHash === receipt.transactionHash,
721
+ )
722
+ expect(row?.apiKeyId).toBe('key_sponsor')
723
+ expect(row?.orgId).toBe('org_1')
724
+ expect(row?.transaction.startsWith('0x76')).toBe(true)
725
+
726
+ // The finalizer resolves the row against the real localnet receipt.
727
+ const result = await Sponsorships.finalize(db, { getClient: () => relayClient })
728
+ expect(result.failed).toBe(0)
729
+ expect(result.finalized).toBeGreaterThanOrEqual(1)
730
+ const finalized = await SponsoredTransactions.get(db, row!.id)
731
+ expect(finalized?.status).toBe('finalized')
732
+ expect(finalized?.feeAmount).toBe(
733
+ Fees.fromGas(receipt.gasUsed, receipt.effectiveGasPrice).toString(),
734
+ )
735
+ }, 30_000)
736
+
737
+ test('behavior: sign-and-broadcast sends record through the relay', async () => {
738
+ const request = await prepareTransactionRequest(relayClient, {
739
+ account: sender,
740
+ calls: [
741
+ Actions.token.transfer.call(relayClient, {
742
+ amount: 2n,
743
+ to: Tempo.accounts[8]!.address,
744
+ token: alphaUsd,
745
+ }),
746
+ ],
747
+ feePayer: true,
748
+ })
749
+ const signed = await signTransaction(relayClient, request)
750
+
751
+ const receipt = await sendRawTransactionSync(getClient('sign-and-broadcast'), {
752
+ serializedTransaction: signed,
753
+ })
754
+
755
+ const rows = await SponsoredTransactions.listPending(db)
756
+ expect(rows.find((row) => row.transactionHash === receipt.transactionHash)).toBeDefined()
757
+ }, 30_000)
758
+ })
759
+
760
+ /**
761
+ * Signs a sponsorship-requesting Tempo transaction (fee-payer slot present but
762
+ * empty; the serializer strips `feeToken`, the sponsor picks it) entirely
763
+ * offline: `eth_signRawTransaction` never touches the chain, so the suite
764
+ * runs without a node.
765
+ */
766
+ async function signSponsorable(
767
+ overrides: {
768
+ chainId?: number
769
+ feeToken?: `0x${string}`
770
+ gas?: bigint
771
+ maxFeePerGas?: bigint
772
+ nonce?: number
773
+ } = {},
774
+ ) {
775
+ return await senderAccount.signTransaction({
776
+ chainId: overrides.chainId ?? runtime.chainId,
777
+ feePayer: true,
778
+ gas: overrides.gas ?? 100_000n,
779
+ maxFeePerGas: overrides.maxFeePerGas ?? 1_000_000n,
780
+ maxPriorityFeePerGas: 0n,
781
+ nonce: overrides.nonce ?? 0,
782
+ to: Tempo.accounts[8]!.address,
783
+ value: 0n,
784
+ ...(overrides.feeToken ? { feeToken: overrides.feeToken } : {}),
785
+ })
786
+ }
787
+
788
+ /** Posts one JSON-RPC request to the composed app's relay mount. */
789
+ async function relayRequest(
790
+ app: ReturnType<typeof TestApp.create>,
791
+ options: { method: string; params: readonly unknown[]; token?: string | undefined },
792
+ ) {
793
+ const response = await app.fetch(
794
+ new Request('http://tempo-api.test/rpc/relay', {
795
+ body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: options.method, params: options.params }), // prettier-ignore
796
+ headers: {
797
+ 'content-type': 'application/json',
798
+ ...(options.token ? { 'tempo-api-key': options.token } : {}),
799
+ },
800
+ method: 'POST',
801
+ }),
802
+ )
803
+ return (await response.json()) as {
804
+ error?: { code: number; data?: { code: string } | undefined; message: string } | undefined
805
+ result?: `0x${string}` | undefined
806
+ }
807
+ }
808
+
809
+ // The full billing-to-invoice flow with nothing synthetic: a disposable node
810
+ // runs under mainnet's chain id (dev consensus, funded dev alloc), so a real
811
+ // card attach opens the real gate, a real sponsored transaction mines with a
812
+ // real receipt, the real finalizer prices it, and real meter events land on a
813
+ // real Stripe invoice.
814
+ describe.runIf(TestStripe.secretKey)(
815
+ 'behavior: sponsored spend reaches the invoice (stripe sandbox)',
816
+ () => {
817
+ // prettier-ignore
818
+ const webhookSecret = `whsec_${'c'.repeat(32)}`
819
+ let node: Awaited<ReturnType<typeof startTempo>> | undefined
820
+
821
+ beforeAll(async () => {
822
+ node = await startTempo({ chainId: Viem.chainId.mainnet })
823
+ }, 240_000)
824
+
825
+ afterAll(async () => {
826
+ await TestStripe.sweep()
827
+ await node?.container.stop()
828
+ })
829
+
830
+ test(
831
+ 'a sponsored transaction meters real usage onto the invoice',
832
+ { retry: 0, timeout: 300_000 },
833
+ async () => {
834
+ // prettier-ignore
835
+ const { url } = node!
836
+ const stripe = TestStripe.client()
837
+ const db = TestApp.database()
838
+ // The key attributes to org_1; the row must exist for the usage read.
839
+ await Organizations.create(db, { id: 'org_1', name: 'Sponsored Spend Org' })
840
+ // Mainnet sponsorship refuses fee tokens the verified list cannot
841
+ // resolve to USD; seed it like the gate tests do.
842
+ await TestApp.verifiedSeed(db, Viem.chainId.mainnet)
843
+ const app = TestApp.create({
844
+ auth: { keys, superAdmin: { secret: superAdminSecret } },
845
+ billing: { stripe: { client: stripe, webhookSecret } },
846
+ db,
847
+ relay: { feePayer: { account: feePayerAccount } },
848
+ rpc: { url: () => url },
849
+ supportedChainIds: [Viem.chainId.mainnet],
850
+ })
851
+ const chainClient = Viem.getClient({
852
+ chainId: Viem.chainId.mainnet,
853
+ rpc: { url: () => url },
854
+ })
855
+ // Both signers pay real fee-token balances on the node.
856
+ await Actions.faucet.fundSync(chainClient, { account: senderAccount.address, timeout: 60_000 }) // prettier-ignore
857
+ await Actions.faucet.fundSync(chainClient, { account: feePayerAccount.address, timeout: 60_000 }) // prettier-ignore
858
+
859
+ // Closed until a real card attach flips the derived status.
860
+ const refused = await relayRequest(app, {
861
+ method: 'eth_signRawTransaction',
862
+ params: [await signSponsorable({ chainId: Viem.chainId.mainnet })],
863
+ token: 'secret_sponsor_prod',
864
+ })
865
+ expect(refused.error?.data).toStrictEqual({ code: 'billing_required' })
866
+
867
+ const customer = await stripe.customers.create({
868
+ metadata: { orgId: 'org_1' },
869
+ name: `Test Org ${nanoid(10)}`,
870
+ })
871
+ TestStripe.track(customer.id)
872
+ await StripeCustomers.create(db, { orgId: 'org_1', stripeCustomerId: customer.id })
873
+ await TestStripe.completeCheckout(app, { customer: customer.id, webhookSecret })
874
+ expect((await StripeCustomers.get(db, 'org_1'))?.status).toBe('active')
875
+
876
+ // Broadcast a sponsored transaction through the relay; it mines locally.
877
+ const sent = await relayRequest(app, {
878
+ method: 'eth_sendRawTransactionSync',
879
+ params: [
880
+ await signSponsorable({
881
+ chainId: Viem.chainId.mainnet,
882
+ gas: 1_000_000n,
883
+ maxFeePerGas: 500_000_000_000n,
884
+ }),
885
+ ],
886
+ token: 'secret_sponsor_prod',
887
+ })
888
+ expect(sent.error).toBeUndefined()
889
+
890
+ // The real receipt finalizes the row with the actual charged fee.
891
+ const finalizer = Sponsorships.createFinalizer({ db, rpc: { url: () => url } })
892
+ await finalizer.tick()
893
+ const [row] = await SponsoredTransactions.listUnreported(db, { chainIds: [Viem.chainId.mainnet] }) // prettier-ignore
894
+ expect(row?.status).toBe('finalized')
895
+ expect(row?.feeMax).toBe('500000')
896
+ const feeAmount = BigInt(row!.feeAmount!)
897
+ expect(feeAmount).toBeGreaterThan(0n)
898
+ expect(feeAmount).toBeLessThanOrEqual(500_000n)
899
+
900
+ // Usage surfaces through the API exactly as enforced.
901
+ const usage = await app.request('/v1/orgs/org_1/billing', {
902
+ headers: { 'tempo-api-key': superAdminSecret },
903
+ })
904
+ expect(usage.status).toBe(200)
905
+ const body = (await usage.json()) as { spend: { amount: string } }
906
+ expect(body.spend.amount).toBe(core_Billing.fromBaseUnits(feeAmount))
907
+
908
+ // The reporter meters the spend; Stripe aggregates it onto the upcoming
909
+ // invoice (asynchronously, so poll).
910
+ expect(await core_Billing.createReporter({ db, stripe }).tick()).toStrictEqual({
911
+ failed: 0,
912
+ reported: 1,
913
+ skipped: 0,
914
+ })
915
+ const { priceId } = await core_Billing.ensureFixtures(stripe)
916
+ const [subscription] = (await stripe.subscriptions.list({ customer: customer.id, limit: 1, price: priceId })).data // prettier-ignore
917
+ expect(subscription).toBeDefined()
918
+ const deadline = Date.now() + 180_000
919
+ let quantity: number | null | undefined
920
+ while (Date.now() < deadline) {
921
+ const preview = await stripe.invoices.createPreview({
922
+ customer: customer.id,
923
+ subscription: subscription!.id,
924
+ })
925
+ quantity = preview.lines.data.find((l) => l.pricing?.price_details?.price === priceId)?.quantity // prettier-ignore
926
+ if (quantity === Number(feeAmount)) break
927
+ await new Promise((resolve) => setTimeout(resolve, 5_000))
928
+ }
929
+ expect(quantity).toBe(Number(feeAmount))
930
+ },
931
+ )
932
+ },
933
+ )