tapimo 0.2.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 (313) hide show
  1. package/README.md +31 -3
  2. package/dist/ApiKey.d.ts +1 -0
  3. package/dist/ApiKey.d.ts.map +1 -1
  4. package/dist/ApiKey.js +5 -2
  5. package/dist/ApiKey.js.map +1 -1
  6. package/dist/ApiKeys.d.ts +28 -1
  7. package/dist/ApiKeys.d.ts.map +1 -1
  8. package/dist/ApiKeys.js +34 -1
  9. package/dist/ApiKeys.js.map +1 -1
  10. package/dist/App.d.ts +7167 -4507
  11. package/dist/App.d.ts.map +1 -1
  12. package/dist/App.js +8 -0
  13. package/dist/App.js.map +1 -1
  14. package/dist/Scope.d.ts +11 -2
  15. package/dist/Scope.d.ts.map +1 -1
  16. package/dist/Scope.js +14 -3
  17. package/dist/Scope.js.map +1 -1
  18. package/dist/admin/App.d.ts +128 -118
  19. package/dist/admin/App.d.ts.map +1 -1
  20. package/dist/admin/apps/api-keys.d.ts +7 -0
  21. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  22. package/dist/admin/apps/scopes.d.ts +8 -0
  23. package/dist/admin/apps/scopes.d.ts.map +1 -1
  24. package/dist/admin/apps/scopes.js +3 -0
  25. package/dist/admin/apps/scopes.js.map +1 -1
  26. package/dist/admin/apps/verified-tokens.d.ts +57 -57
  27. package/dist/admin/ui.generated.d.ts +1 -1
  28. package/dist/admin/ui.generated.js +13 -13
  29. package/dist/admin/ui.generated.js.map +1 -1
  30. package/dist/analytics/Analytics.d.ts +151 -0
  31. package/dist/analytics/Analytics.d.ts.map +1 -0
  32. package/dist/analytics/Analytics.js +212 -0
  33. package/dist/analytics/Analytics.js.map +1 -0
  34. package/dist/analytics/tables/requestEvents.d.ts +215 -0
  35. package/dist/analytics/tables/requestEvents.d.ts.map +1 -0
  36. package/dist/analytics/tables/requestEvents.js +264 -0
  37. package/dist/analytics/tables/requestEvents.js.map +1 -0
  38. package/dist/apps/data/App.d.ts +1024 -624
  39. package/dist/apps/data/App.d.ts.map +1 -1
  40. package/dist/apps/data/App.js +36 -0
  41. package/dist/apps/data/App.js.map +1 -1
  42. package/dist/apps/data/routes/balances.d.ts +45 -45
  43. package/dist/apps/data/routes/coingecko.d.ts +176 -176
  44. package/dist/apps/data/routes/exchanges.d.ts +455 -455
  45. package/dist/apps/data/routes/fee-amm.d.ts +101 -101
  46. package/dist/apps/data/routes/funding.d.ts +765 -0
  47. package/dist/apps/data/routes/funding.d.ts.map +1 -0
  48. package/dist/apps/data/routes/funding.js +383 -0
  49. package/dist/apps/data/routes/funding.js.map +1 -0
  50. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  51. package/dist/apps/data/routes/receipts.js +2 -8
  52. package/dist/apps/data/routes/receipts.js.map +1 -1
  53. package/dist/apps/data/routes/rpc.d.ts +29 -36
  54. package/dist/apps/data/routes/rpc.d.ts.map +1 -1
  55. package/dist/apps/data/routes/rpc.js +29 -1
  56. package/dist/apps/data/routes/rpc.js.map +1 -1
  57. package/dist/apps/data/routes/tokenlist.d.ts +36 -36
  58. package/dist/apps/data/routes/transfers.d.ts +56 -56
  59. package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
  60. package/dist/apps/data/routes/webhooks.d.ts +45 -45
  61. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  62. package/dist/apps/data/routes/webhooks.js +8 -8
  63. package/dist/apps/data/routes/webhooks.js.map +1 -1
  64. package/dist/apps/docs/App.d.ts +4 -2
  65. package/dist/apps/docs/App.d.ts.map +1 -1
  66. package/dist/apps/docs/App.js +27 -95
  67. package/dist/apps/docs/App.js.map +1 -1
  68. package/dist/apps/docs/reference.d.ts +21 -0
  69. package/dist/apps/docs/reference.d.ts.map +1 -0
  70. package/dist/apps/docs/reference.js +112 -0
  71. package/dist/apps/docs/reference.js.map +1 -0
  72. package/dist/apps/index.d.ts +1 -0
  73. package/dist/apps/index.d.ts.map +1 -1
  74. package/dist/apps/index.js +1 -0
  75. package/dist/apps/index.js.map +1 -1
  76. package/dist/apps/mcp.d.ts +76 -0
  77. package/dist/apps/mcp.d.ts.map +1 -0
  78. package/dist/apps/mcp.js +188 -0
  79. package/dist/apps/mcp.js.map +1 -0
  80. package/dist/apps/platform/App.d.ts +3005 -728
  81. package/dist/apps/platform/App.d.ts.map +1 -1
  82. package/dist/apps/platform/App.js +13 -3
  83. package/dist/apps/platform/App.js.map +1 -1
  84. package/dist/apps/platform/Billing.d.ts +279 -0
  85. package/dist/apps/platform/Billing.d.ts.map +1 -0
  86. package/dist/apps/platform/Billing.js +477 -0
  87. package/dist/apps/platform/Billing.js.map +1 -0
  88. package/dist/apps/platform/index.d.ts +1 -0
  89. package/dist/apps/platform/index.d.ts.map +1 -1
  90. package/dist/apps/platform/index.js +1 -0
  91. package/dist/apps/platform/index.js.map +1 -1
  92. package/dist/apps/platform/routes/api-keys.d.ts +341 -49
  93. package/dist/apps/platform/routes/api-keys.d.ts.map +1 -1
  94. package/dist/apps/platform/routes/api-keys.js +115 -13
  95. package/dist/apps/platform/routes/api-keys.js.map +1 -1
  96. package/dist/apps/platform/routes/billing.d.ts +1520 -0
  97. package/dist/apps/platform/routes/billing.d.ts.map +1 -0
  98. package/dist/apps/platform/routes/billing.js +795 -0
  99. package/dist/apps/platform/routes/billing.js.map +1 -0
  100. package/dist/apps/platform/routes/invitations.d.ts +201 -83
  101. package/dist/apps/platform/routes/invitations.d.ts.map +1 -1
  102. package/dist/apps/platform/routes/invitations.js +58 -0
  103. package/dist/apps/platform/routes/invitations.js.map +1 -1
  104. package/dist/apps/platform/routes/me.d.ts +17 -17
  105. package/dist/apps/platform/routes/orgs.d.ts +50 -36
  106. package/dist/apps/platform/routes/orgs.d.ts.map +1 -1
  107. package/dist/apps/platform/routes/orgs.js +14 -3
  108. package/dist/apps/platform/routes/orgs.js.map +1 -1
  109. package/dist/apps/platform/routes/projects.d.ts +97 -97
  110. package/dist/apps/platform/routes/scopes.d.ts +107 -0
  111. package/dist/apps/platform/routes/scopes.d.ts.map +1 -0
  112. package/dist/apps/platform/routes/scopes.js +66 -0
  113. package/dist/apps/platform/routes/scopes.js.map +1 -0
  114. package/dist/apps/platform/routes/usage.d.ts +588 -0
  115. package/dist/apps/platform/routes/usage.d.ts.map +1 -0
  116. package/dist/apps/platform/routes/usage.js +391 -0
  117. package/dist/apps/platform/routes/usage.js.map +1 -0
  118. package/dist/apps/relay/App.d.ts +4 -1
  119. package/dist/apps/relay/App.d.ts.map +1 -1
  120. package/dist/apps/relay/App.js +118 -10
  121. package/dist/apps/relay/App.js.map +1 -1
  122. package/dist/apps/relay/Sponsorships.d.ts.map +1 -1
  123. package/dist/apps/relay/Sponsorships.js +2 -1
  124. package/dist/apps/relay/Sponsorships.js.map +1 -1
  125. package/dist/cli/Cli.d.ts +8 -1
  126. package/dist/cli/Cli.d.ts.map +1 -1
  127. package/dist/cli/Cli.js +11 -3
  128. package/dist/cli/Cli.js.map +1 -1
  129. package/dist/cli/commands/admin.d.ts +4 -0
  130. package/dist/cli/commands/admin.d.ts.map +1 -1
  131. package/dist/cli/commands/admin.js +80 -1
  132. package/dist/cli/commands/admin.js.map +1 -1
  133. package/dist/db/Db.d.ts +7 -0
  134. package/dist/db/Db.d.ts.map +1 -1
  135. package/dist/db/Db.js +126 -0
  136. package/dist/db/Db.js.map +1 -1
  137. package/dist/db/Schema.d.ts +79 -0
  138. package/dist/db/Schema.d.ts.map +1 -1
  139. package/dist/db/Schema.js +88 -2
  140. package/dist/db/Schema.js.map +1 -1
  141. package/dist/db/tables/billingSettings.d.ts +55 -0
  142. package/dist/db/tables/billingSettings.d.ts.map +1 -0
  143. package/dist/db/tables/billingSettings.js +70 -0
  144. package/dist/db/tables/billingSettings.js.map +1 -0
  145. package/dist/db/tables/invitations.js +2 -2
  146. package/dist/db/tables/invitations.js.map +1 -1
  147. package/dist/db/tables/organizations.d.ts +6 -2
  148. package/dist/db/tables/organizations.d.ts.map +1 -1
  149. package/dist/db/tables/organizations.js +10 -4
  150. package/dist/db/tables/organizations.js.map +1 -1
  151. package/dist/db/tables/projects.js +2 -2
  152. package/dist/db/tables/projects.js.map +1 -1
  153. package/dist/db/tables/requestUsage.d.ts +121 -0
  154. package/dist/db/tables/requestUsage.d.ts.map +1 -0
  155. package/dist/db/tables/requestUsage.js +150 -0
  156. package/dist/db/tables/requestUsage.js.map +1 -0
  157. package/dist/db/tables/sponsoredTransactions.d.ts +119 -0
  158. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  159. package/dist/db/tables/sponsoredTransactions.js +155 -2
  160. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  161. package/dist/db/tables/stripeCustomers.d.ts +81 -0
  162. package/dist/db/tables/stripeCustomers.d.ts.map +1 -0
  163. package/dist/db/tables/stripeCustomers.js +120 -0
  164. package/dist/db/tables/stripeCustomers.js.map +1 -0
  165. package/dist/db/tables/users.js +2 -2
  166. package/dist/db/tables/users.js.map +1 -1
  167. package/dist/handlers/internal/sponsorship.d.ts +5 -2
  168. package/dist/handlers/internal/sponsorship.d.ts.map +1 -1
  169. package/dist/handlers/internal/sponsorship.js +19 -5
  170. package/dist/handlers/internal/sponsorship.js.map +1 -1
  171. package/dist/handlers/internal/utils.d.ts +10 -3
  172. package/dist/handlers/internal/utils.d.ts.map +1 -1
  173. package/dist/handlers/internal/utils.js +14 -6
  174. package/dist/handlers/internal/utils.js.map +1 -1
  175. package/dist/index.d.ts +1 -0
  176. package/dist/index.d.ts.map +1 -1
  177. package/dist/index.js +1 -0
  178. package/dist/index.js.map +1 -1
  179. package/dist/internal/Auth.d.ts +18 -4
  180. package/dist/internal/Auth.d.ts.map +1 -1
  181. package/dist/internal/Auth.js +148 -76
  182. package/dist/internal/Auth.js.map +1 -1
  183. package/dist/internal/Cache.d.ts.map +1 -1
  184. package/dist/internal/Cache.js +4 -1
  185. package/dist/internal/Cache.js.map +1 -1
  186. package/dist/internal/EdgeCache.d.ts +10 -0
  187. package/dist/internal/EdgeCache.d.ts.map +1 -1
  188. package/dist/internal/EdgeCache.js +39 -8
  189. package/dist/internal/EdgeCache.js.map +1 -1
  190. package/dist/internal/Fees.d.ts +27 -0
  191. package/dist/internal/Fees.d.ts.map +1 -0
  192. package/dist/internal/Fees.js +48 -0
  193. package/dist/internal/Fees.js.map +1 -0
  194. package/dist/internal/FundingRoutes.d.ts +183 -0
  195. package/dist/internal/FundingRoutes.d.ts.map +1 -0
  196. package/dist/internal/FundingRoutes.js +490 -0
  197. package/dist/internal/FundingRoutes.js.map +1 -0
  198. package/dist/internal/Id.d.ts +15 -0
  199. package/dist/internal/Id.d.ts.map +1 -0
  200. package/dist/internal/Id.js +22 -0
  201. package/dist/internal/Id.js.map +1 -0
  202. package/dist/internal/Log.d.ts +2 -0
  203. package/dist/internal/Log.d.ts.map +1 -1
  204. package/dist/internal/Log.js +3 -0
  205. package/dist/internal/Log.js.map +1 -1
  206. package/dist/internal/OpenApi.d.ts +10 -1
  207. package/dist/internal/OpenApi.d.ts.map +1 -1
  208. package/dist/internal/OpenApi.js +8 -1
  209. package/dist/internal/OpenApi.js.map +1 -1
  210. package/dist/internal/RateLimit.d.ts +25 -0
  211. package/dist/internal/RateLimit.d.ts.map +1 -1
  212. package/dist/internal/RateLimit.js +51 -0
  213. package/dist/internal/RateLimit.js.map +1 -1
  214. package/dist/internal/Response.d.ts +1 -1
  215. package/dist/internal/Response.d.ts.map +1 -1
  216. package/dist/internal/Schema.d.ts +8 -0
  217. package/dist/internal/Schema.d.ts.map +1 -1
  218. package/dist/internal/Schema.js +7 -0
  219. package/dist/internal/Schema.js.map +1 -1
  220. package/dist/internal/Webhooks.d.ts.map +1 -1
  221. package/dist/internal/Webhooks.js +3 -3
  222. package/dist/internal/Webhooks.js.map +1 -1
  223. package/package.json +4 -3
  224. package/src/ApiKey.ts +9 -2
  225. package/src/ApiKeys.test.ts +51 -0
  226. package/src/ApiKeys.ts +50 -1
  227. package/src/App.test.ts +33 -86718
  228. package/src/App.ts +18 -0
  229. package/src/Client.test-d.ts +351 -0
  230. package/src/Scope.ts +16 -3
  231. package/src/admin/apps/scopes.test.ts +6 -0
  232. package/src/admin/apps/scopes.ts +3 -0
  233. package/src/admin/ui/src/components/Page.tsx +2 -3
  234. package/src/admin/ui/src/routes/__root.tsx +2 -2
  235. package/src/admin/ui/src/routes/api-keys.tsx +2 -2
  236. package/src/admin/ui.generated.ts +13 -13
  237. package/src/analytics/Analytics.test.ts +527 -0
  238. package/src/analytics/Analytics.ts +318 -0
  239. package/src/analytics/tables/requestEvents.ts +523 -0
  240. package/src/apps/data/App.test.ts +72 -0
  241. package/src/apps/data/App.ts +35 -0
  242. package/src/apps/data/routes/funding.test.ts +214 -0
  243. package/src/apps/data/routes/funding.ts +584 -0
  244. package/src/apps/data/routes/receipts.ts +2 -9
  245. package/src/apps/data/routes/rpc.test.ts +78 -2
  246. package/src/apps/data/routes/rpc.ts +31 -1
  247. package/src/apps/data/routes/webhooks.test.ts +1 -1
  248. package/src/apps/data/routes/webhooks.ts +11 -8
  249. package/src/apps/docs/App.ts +29 -100
  250. package/src/apps/docs/reference.ts +132 -0
  251. package/src/apps/index.ts +1 -0
  252. package/src/apps/mcp.test.ts +206 -0
  253. package/src/apps/mcp.ts +250 -0
  254. package/src/apps/platform/App.ts +43 -4
  255. package/src/apps/platform/Billing.test.ts +530 -0
  256. package/src/apps/platform/Billing.ts +682 -0
  257. package/src/apps/platform/index.ts +1 -0
  258. package/src/apps/platform/routes/api-keys.test.ts +172 -9
  259. package/src/apps/platform/routes/api-keys.ts +136 -20
  260. package/src/apps/platform/routes/billing.test.ts +1100 -0
  261. package/src/apps/platform/routes/billing.ts +966 -0
  262. package/src/apps/platform/routes/invitations.test.ts +55 -7
  263. package/src/apps/platform/routes/invitations.ts +68 -0
  264. package/src/apps/platform/routes/me.test.ts +230 -0
  265. package/src/apps/platform/routes/orgs.ts +26 -4
  266. package/src/apps/platform/routes/scopes.test.ts +29 -0
  267. package/src/apps/platform/routes/scopes.ts +85 -0
  268. package/src/apps/platform/routes/usage.test.ts +422 -0
  269. package/src/apps/platform/routes/usage.ts +527 -0
  270. package/src/apps/relay/App.test.ts +457 -8
  271. package/src/apps/relay/App.ts +120 -12
  272. package/src/apps/relay/Sponsorships.test.ts +1 -1
  273. package/src/apps/relay/Sponsorships.ts +2 -1
  274. package/src/cli/Cli.test.ts +25 -2
  275. package/src/cli/Cli.ts +17 -2
  276. package/src/cli/commands/admin.test.ts +83 -0
  277. package/src/cli/commands/admin.ts +89 -1
  278. package/src/db/Db.ts +137 -0
  279. package/src/db/Schema.ts +104 -2
  280. package/src/db/tables/billingSettings.test.ts +87 -0
  281. package/src/db/tables/billingSettings.ts +108 -0
  282. package/src/db/tables/invitations.ts +2 -2
  283. package/src/db/tables/organizations.test.ts +1 -1
  284. package/src/db/tables/organizations.ts +15 -5
  285. package/src/db/tables/projects.ts +2 -2
  286. package/src/db/tables/requestUsage.ts +231 -0
  287. package/src/db/tables/sponsoredTransactions.test.ts +108 -1
  288. package/src/db/tables/sponsoredTransactions.ts +230 -2
  289. package/src/db/tables/stripeCustomers.test.ts +109 -0
  290. package/src/db/tables/stripeCustomers.ts +162 -0
  291. package/src/db/tables/users.ts +2 -2
  292. package/src/handlers/internal/sponsorship.ts +34 -7
  293. package/src/handlers/internal/utils.ts +19 -7
  294. package/src/index.ts +1 -0
  295. package/src/internal/Auth.test.ts +175 -25
  296. package/src/internal/Auth.ts +175 -86
  297. package/src/internal/Cache.ts +5 -1
  298. package/src/internal/EdgeCache.test.ts +27 -4
  299. package/src/internal/EdgeCache.ts +47 -9
  300. package/src/internal/Fees.test.ts +32 -0
  301. package/src/internal/Fees.ts +51 -0
  302. package/src/internal/FundingRoutes.ts +583 -0
  303. package/src/internal/Id.test.ts +17 -0
  304. package/src/internal/Id.ts +24 -0
  305. package/src/internal/Log.test.ts +1 -0
  306. package/src/internal/Log.ts +5 -0
  307. package/src/internal/OpenApi.ts +12 -1
  308. package/src/internal/RateLimit.test.ts +84 -0
  309. package/src/internal/RateLimit.ts +70 -0
  310. package/src/internal/Response.ts +1 -1
  311. package/src/internal/Schema.ts +8 -0
  312. package/src/internal/Webhooks.test.ts +3 -3
  313. package/src/internal/Webhooks.ts +3 -3
@@ -1,14 +1,24 @@
1
+ import { nanoid } from 'nanoid'
1
2
  import { Hash, Secp256k1 } from 'ox'
2
3
  import { createClient, http } from 'viem'
3
4
  import { prepareTransactionRequest, sendRawTransactionSync, signTransaction } from 'viem/actions'
4
5
  import { Account as TempoAccount, Actions, Transaction, withRelay } from 'viem/tempo'
5
6
 
6
7
  import * as TestApp from '../../../test/App.js'
8
+ import { startTempo } from '../../../test/containers.js'
7
9
  import * as Relay from '../../../test/Relay.js'
8
10
  import * as Runtime from '../../../test/runtime.js'
9
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'
10
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'
11
20
  import * as RequestListener from '../../handlers/internal/requestListener.js'
21
+ import * as Viem from '../../internal/Viem.js'
12
22
  import * as Sponsorships from './Sponsorships.js'
13
23
 
14
24
  const runtime = Runtime.get()
@@ -16,6 +26,9 @@ const runtime = Runtime.get()
16
26
  const feePayerAccount = Tempo.accounts[10]!
17
27
  const senderAccount = Tempo.accounts[9]!
18
28
 
29
+ /** Super admin secret configured on the end-to-end billing app. */
30
+ const superAdminSecret = 'tempo:sk:a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1'
31
+
19
32
  /** Keys covering the sponsorship policy matrix; all hold the base relay scope. */
20
33
  const keys = [
21
34
  {
@@ -42,6 +55,14 @@ const keys = [
42
55
  scopes: ['*'],
43
56
  token: 'secret_wildcard',
44
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
+ },
45
66
  {
46
67
  id: 'key_unscoped',
47
68
  orgId: 'org_1',
@@ -91,11 +112,14 @@ describe.skipIf(runtime.mode !== 'localnet')('behavior: sponsorship', () => {
91
112
  "billable": false,
92
113
  "chainId": "<chainId>",
93
114
  "createdAt": "<timestamp>",
115
+ "currency": null,
94
116
  "environment": "sandbox",
95
117
  "feeAmount": null,
118
+ "feeMax": "1",
96
119
  "feeToken": "0x20c0000000000000000000000000000000000000",
97
120
  "finalizedAt": null,
98
121
  "id": "<id>",
122
+ "meterReportedAt": null,
99
123
  "orgId": "org_1",
100
124
  "projectId": "prj_1",
101
125
  "signPayload": "<hash>",
@@ -304,6 +328,292 @@ describe.skipIf(runtime.mode !== 'localnet')('behavior: sponsorship', () => {
304
328
  })
305
329
  })
306
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
+
307
617
  describe.skipIf(runtime.mode !== 'localnet')('behavior: sponsorship (viem)', () => {
308
618
  const alphaUsd = '0x20c0000000000000000000000000000000000001'
309
619
  const feePayer = TempoAccount.fromSecp256k1(Secp256k1.randomPrivateKey())
@@ -383,7 +693,9 @@ describe.skipIf(runtime.mode !== 'localnet')('behavior: sponsorship (viem)', ()
383
693
  }
384
694
  expect(finalized?.status).toBe('finalized')
385
695
  expect(finalized?.transactionHash).toBe(receipt.transactionHash)
386
- expect(finalized?.feeAmount).toBe((receipt.gasUsed * receipt.effectiveGasPrice).toString())
696
+ expect(finalized?.feeAmount).toBe(
697
+ Fees.fromGas(receipt.gasUsed, receipt.effectiveGasPrice).toString(),
698
+ )
387
699
  }, 60_000)
388
700
 
389
701
  test('behavior: sign-only sends record and finalize from the real receipt', async () => {
@@ -417,7 +729,9 @@ describe.skipIf(runtime.mode !== 'localnet')('behavior: sponsorship (viem)', ()
417
729
  expect(result.finalized).toBeGreaterThanOrEqual(1)
418
730
  const finalized = await SponsoredTransactions.get(db, row!.id)
419
731
  expect(finalized?.status).toBe('finalized')
420
- expect(finalized?.feeAmount).toBe((receipt.gasUsed * receipt.effectiveGasPrice).toString())
732
+ expect(finalized?.feeAmount).toBe(
733
+ Fees.fromGas(receipt.gasUsed, receipt.effectiveGasPrice).toString(),
734
+ )
421
735
  }, 30_000)
422
736
 
423
737
  test('behavior: sign-and-broadcast sends record through the relay', async () => {
@@ -449,16 +763,25 @@ describe.skipIf(runtime.mode !== 'localnet')('behavior: sponsorship (viem)', ()
449
763
  * offline: `eth_signRawTransaction` never touches the chain, so the suite
450
764
  * runs without a node.
451
765
  */
452
- async function signSponsorable() {
766
+ async function signSponsorable(
767
+ overrides: {
768
+ chainId?: number
769
+ feeToken?: `0x${string}`
770
+ gas?: bigint
771
+ maxFeePerGas?: bigint
772
+ nonce?: number
773
+ } = {},
774
+ ) {
453
775
  return await senderAccount.signTransaction({
454
- chainId: runtime.chainId,
776
+ chainId: overrides.chainId ?? runtime.chainId,
455
777
  feePayer: true,
456
- gas: 100_000n,
457
- maxFeePerGas: 1_000_000n,
778
+ gas: overrides.gas ?? 100_000n,
779
+ maxFeePerGas: overrides.maxFeePerGas ?? 1_000_000n,
458
780
  maxPriorityFeePerGas: 0n,
459
- nonce: 0,
781
+ nonce: overrides.nonce ?? 0,
460
782
  to: Tempo.accounts[8]!.address,
461
783
  value: 0n,
784
+ ...(overrides.feeToken ? { feeToken: overrides.feeToken } : {}),
462
785
  })
463
786
  }
464
787
 
@@ -478,7 +801,133 @@ async function relayRequest(
478
801
  }),
479
802
  )
480
803
  return (await response.json()) as {
481
- error?: { code: number; message: string } | undefined
804
+ error?: { code: number; data?: { code: string } | undefined; message: string } | undefined
482
805
  result?: `0x${string}` | undefined
483
806
  }
484
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
+ )