tapimo 0.12.0 → 0.14.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 (398) hide show
  1. package/dist/App.d.ts +11333 -9563
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +39 -3
  4. package/dist/App.js.map +1 -1
  5. package/dist/Scope.d.ts +2 -0
  6. package/dist/Scope.d.ts.map +1 -1
  7. package/dist/Scope.js +17 -0
  8. package/dist/Scope.js.map +1 -1
  9. package/dist/Webhooks.js +2 -2
  10. package/dist/Webhooks.js.map +1 -1
  11. package/dist/admin/App.d.ts +1064 -924
  12. package/dist/admin/App.d.ts.map +1 -1
  13. package/dist/admin/apps/api-keys.d.ts +80 -0
  14. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  15. package/dist/admin/apps/api-keys.js +54 -0
  16. package/dist/admin/apps/api-keys.js.map +1 -1
  17. package/dist/admin/apps/earn-vaults.d.ts +63 -15
  18. package/dist/admin/apps/earn-vaults.d.ts.map +1 -1
  19. package/dist/admin/apps/earn-vaults.js +2 -1
  20. package/dist/admin/apps/earn-vaults.js.map +1 -1
  21. package/dist/admin/apps/organizations.d.ts +92 -3
  22. package/dist/admin/apps/organizations.d.ts.map +1 -1
  23. package/dist/admin/apps/organizations.js +69 -7
  24. package/dist/admin/apps/organizations.js.map +1 -1
  25. package/dist/admin/apps/verified-tokens.d.ts +7 -7
  26. package/dist/admin/ui.generated.d.ts +1 -1
  27. package/dist/admin/ui.generated.d.ts.map +1 -1
  28. package/dist/admin/ui.generated.js +151 -151
  29. package/dist/admin/ui.generated.js.map +1 -1
  30. package/dist/apps/Zones.d.ts +28 -28
  31. package/dist/apps/Zones.js +7 -7
  32. package/dist/apps/data/App.d.ts +2909 -2523
  33. package/dist/apps/data/App.d.ts.map +1 -1
  34. package/dist/apps/data/App.js +26 -21
  35. package/dist/apps/data/App.js.map +1 -1
  36. package/dist/apps/data/ZoneSelection.d.ts +8 -8
  37. package/dist/apps/data/ZoneSelection.d.ts.map +1 -1
  38. package/dist/apps/data/ZoneSelection.js +1 -1
  39. package/dist/apps/data/ZoneSelection.js.map +1 -1
  40. package/dist/apps/data/routes/activities.d.ts +588 -584
  41. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  42. package/dist/apps/data/routes/activities.js +1561 -1239
  43. package/dist/apps/data/routes/activities.js.map +1 -1
  44. package/dist/apps/data/routes/addresses.d.ts +22 -22
  45. package/dist/apps/data/routes/balances.d.ts +26 -26
  46. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  47. package/dist/apps/data/routes/balances.js +43 -16
  48. package/dist/apps/data/routes/balances.js.map +1 -1
  49. package/dist/apps/data/routes/blocks.d.ts +44 -44
  50. package/dist/apps/data/routes/blocks.d.ts.map +1 -1
  51. package/dist/apps/data/routes/blocks.js +21 -11
  52. package/dist/apps/data/routes/blocks.js.map +1 -1
  53. package/dist/apps/data/routes/coingecko.d.ts +3 -3
  54. package/dist/apps/data/routes/earn.d.ts +201 -25
  55. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  56. package/dist/apps/data/routes/earn.js +434 -75
  57. package/dist/apps/data/routes/earn.js.map +1 -1
  58. package/dist/apps/data/routes/exchanges.d.ts +160 -145
  59. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  60. package/dist/apps/data/routes/exchanges.js +180 -114
  61. package/dist/apps/data/routes/exchanges.js.map +1 -1
  62. package/dist/apps/data/routes/fee-amm.d.ts +2 -2
  63. package/dist/apps/data/routes/fee-amm.d.ts.map +1 -1
  64. package/dist/apps/data/routes/fee-amm.js +18 -19
  65. package/dist/apps/data/routes/fee-amm.js.map +1 -1
  66. package/dist/apps/data/routes/receipts.d.ts +155 -55
  67. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  68. package/dist/apps/data/routes/receipts.js +28 -10
  69. package/dist/apps/data/routes/receipts.js.map +1 -1
  70. package/dist/apps/data/routes/tokenlist.d.ts +13 -13
  71. package/dist/apps/data/routes/tokens.d.ts +5 -5
  72. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  73. package/dist/apps/data/routes/tokens.js +51 -20
  74. package/dist/apps/data/routes/tokens.js.map +1 -1
  75. package/dist/apps/data/routes/transactions.d.ts +7 -2
  76. package/dist/apps/data/routes/transactions.d.ts.map +1 -1
  77. package/dist/apps/data/routes/transactions.js +37 -17
  78. package/dist/apps/data/routes/transactions.js.map +1 -1
  79. package/dist/apps/data/routes/transfers.d.ts +13 -13
  80. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  81. package/dist/apps/data/routes/transfers.js +18 -12
  82. package/dist/apps/data/routes/transfers.js.map +1 -1
  83. package/dist/apps/data/routes/valuation.d.ts +31 -31
  84. package/dist/apps/data/routes/valuation.d.ts.map +1 -1
  85. package/dist/apps/data/routes/valuation.js +12 -7
  86. package/dist/apps/data/routes/valuation.js.map +1 -1
  87. package/dist/apps/data/routes/verified-tokens.d.ts +8 -8
  88. package/dist/apps/data/routes/webhooks.d.ts +816 -113
  89. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  90. package/dist/apps/data/routes/webhooks.js +47 -5
  91. package/dist/apps/data/routes/webhooks.js.map +1 -1
  92. package/dist/apps/data/routes/zones.d.ts +14 -14
  93. package/dist/apps/data/routes/zones.js +1 -1
  94. package/dist/apps/funding/App.d.ts +466 -44
  95. package/dist/apps/funding/App.d.ts.map +1 -1
  96. package/dist/apps/funding/App.js +1 -1
  97. package/dist/apps/funding/App.js.map +1 -1
  98. package/dist/apps/funding/routes/deposit-addresses.d.ts +17 -17
  99. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
  100. package/dist/apps/funding/routes/deposit-addresses.js +17 -12
  101. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
  102. package/dist/apps/funding/routes/transfers.d.ts +650 -57
  103. package/dist/apps/funding/routes/transfers.d.ts.map +1 -1
  104. package/dist/apps/funding/routes/transfers.js +238 -24
  105. package/dist/apps/funding/routes/transfers.js.map +1 -1
  106. package/dist/apps/management/App.d.ts +5120 -4152
  107. package/dist/apps/management/App.d.ts.map +1 -1
  108. package/dist/apps/management/Billing.d.ts +1 -1
  109. package/dist/apps/management/Billing.d.ts.map +1 -1
  110. package/dist/apps/management/Billing.js +17 -1
  111. package/dist/apps/management/Billing.js.map +1 -1
  112. package/dist/apps/management/routes/api-keys.d.ts +632 -81
  113. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  114. package/dist/apps/management/routes/api-keys.js +236 -4
  115. package/dist/apps/management/routes/api-keys.js.map +1 -1
  116. package/dist/apps/management/routes/billing.d.ts +76 -76
  117. package/dist/apps/management/routes/faucet.d.ts +6 -6
  118. package/dist/apps/management/routes/invitations.d.ts +24 -24
  119. package/dist/apps/management/routes/invite-links.d.ts +4 -4
  120. package/dist/apps/management/routes/members.d.ts +16 -16
  121. package/dist/apps/management/routes/orgs.d.ts +20 -20
  122. package/dist/apps/management/routes/projects.d.ts +4 -4
  123. package/dist/apps/management/routes/usage.d.ts +8 -8
  124. package/dist/apps/management/routes/verified-token-requests.d.ts +17 -17
  125. package/dist/apps/management/routes/webhooks.d.ts +675 -25
  126. package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
  127. package/dist/apps/management/routes/webhooks.js +21 -21
  128. package/dist/apps/management/routes/webhooks.js.map +1 -1
  129. package/dist/apps/relay/App.d.ts.map +1 -1
  130. package/dist/apps/relay/App.js +115 -23
  131. package/dist/apps/relay/App.js.map +1 -1
  132. package/dist/apps/relay/Sponsorships.d.ts.map +1 -1
  133. package/dist/apps/relay/Sponsorships.js +27 -1
  134. package/dist/apps/relay/Sponsorships.js.map +1 -1
  135. package/dist/db/Db.d.ts.map +1 -1
  136. package/dist/db/Db.js +34 -1
  137. package/dist/db/Db.js.map +1 -1
  138. package/dist/db/Schema.d.ts +11 -5
  139. package/dist/db/Schema.d.ts.map +1 -1
  140. package/dist/db/Schema.js +21 -3
  141. package/dist/db/Schema.js.map +1 -1
  142. package/dist/db/tables/fundingCatalog.d.ts +4 -2
  143. package/dist/db/tables/fundingCatalog.d.ts.map +1 -1
  144. package/dist/db/tables/fundingCatalog.js +5 -1
  145. package/dist/db/tables/fundingCatalog.js.map +1 -1
  146. package/dist/db/tables/fundingTransferTransactions.d.ts +38 -0
  147. package/dist/db/tables/fundingTransferTransactions.d.ts.map +1 -1
  148. package/dist/db/tables/fundingTransferTransactions.js +44 -3
  149. package/dist/db/tables/fundingTransferTransactions.js.map +1 -1
  150. package/dist/db/tables/fundingTransfers.d.ts +17 -0
  151. package/dist/db/tables/fundingTransfers.d.ts.map +1 -1
  152. package/dist/db/tables/fundingTransfers.js +21 -0
  153. package/dist/db/tables/fundingTransfers.js.map +1 -1
  154. package/dist/db/tables/organizations.d.ts +24 -0
  155. package/dist/db/tables/organizations.d.ts.map +1 -1
  156. package/dist/db/tables/organizations.js +27 -0
  157. package/dist/db/tables/organizations.js.map +1 -1
  158. package/dist/db/tables/projects.d.ts +25 -1
  159. package/dist/db/tables/projects.d.ts.map +1 -1
  160. package/dist/db/tables/projects.js +26 -0
  161. package/dist/db/tables/projects.js.map +1 -1
  162. package/dist/db/tables/sponsoredTransactions.d.ts +44 -0
  163. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  164. package/dist/db/tables/sponsoredTransactions.js +146 -13
  165. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  166. package/dist/db/tables/webhookSubscriptions.d.ts +10 -6
  167. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  168. package/dist/db/tables/webhookSubscriptions.js +9 -6
  169. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  170. package/dist/handlers/internal/utils.d.ts.map +1 -1
  171. package/dist/handlers/internal/utils.js +9 -0
  172. package/dist/handlers/internal/utils.js.map +1 -1
  173. package/dist/internal/Auth.d.ts +3 -1
  174. package/dist/internal/Auth.d.ts.map +1 -1
  175. package/dist/internal/Auth.js +6 -3
  176. package/dist/internal/Auth.js.map +1 -1
  177. package/dist/internal/EarnVaults.d.ts +3 -3
  178. package/dist/internal/EarnVaults.js +5 -5
  179. package/dist/internal/EarnVaults.js.map +1 -1
  180. package/dist/internal/Fetch.d.ts +23 -0
  181. package/dist/internal/Fetch.d.ts.map +1 -0
  182. package/dist/internal/Fetch.js +26 -0
  183. package/dist/internal/Fetch.js.map +1 -0
  184. package/dist/internal/Log.d.ts +4 -2
  185. package/dist/internal/Log.d.ts.map +1 -1
  186. package/dist/internal/Log.js +16 -3
  187. package/dist/internal/Log.js.map +1 -1
  188. package/dist/internal/OpenApi.d.ts +5 -3
  189. package/dist/internal/OpenApi.d.ts.map +1 -1
  190. package/dist/internal/OpenApi.js +13 -3
  191. package/dist/internal/OpenApi.js.map +1 -1
  192. package/dist/internal/Response.d.ts +1 -1
  193. package/dist/internal/Response.d.ts.map +1 -1
  194. package/dist/internal/Schema.d.ts +6 -3
  195. package/dist/internal/Schema.d.ts.map +1 -1
  196. package/dist/internal/Schema.js +8 -2
  197. package/dist/internal/Schema.js.map +1 -1
  198. package/dist/internal/Store.d.ts +16 -3
  199. package/dist/internal/Store.d.ts.map +1 -1
  200. package/dist/internal/Store.js +82 -16
  201. package/dist/internal/Store.js.map +1 -1
  202. package/dist/internal/Tidx.d.ts.map +1 -1
  203. package/dist/internal/Tidx.js +7 -2
  204. package/dist/internal/Tidx.js.map +1 -1
  205. package/dist/internal/Timeout.d.ts +36 -0
  206. package/dist/internal/Timeout.d.ts.map +1 -0
  207. package/dist/internal/Timeout.js +37 -0
  208. package/dist/internal/Timeout.js.map +1 -0
  209. package/dist/internal/Timing.d.ts +9 -0
  210. package/dist/internal/Timing.d.ts.map +1 -1
  211. package/dist/internal/Timing.js +7 -0
  212. package/dist/internal/Timing.js.map +1 -1
  213. package/dist/internal/Viem.d.ts +16 -1
  214. package/dist/internal/Viem.d.ts.map +1 -1
  215. package/dist/internal/Viem.js +12 -2
  216. package/dist/internal/Viem.js.map +1 -1
  217. package/dist/internal/WebhookDestination.d.ts.map +1 -1
  218. package/dist/internal/WebhookDestination.js +3 -0
  219. package/dist/internal/WebhookDestination.js.map +1 -1
  220. package/dist/internal/funding/Action.d.ts +9 -0
  221. package/dist/internal/funding/Action.d.ts.map +1 -1
  222. package/dist/internal/funding/Action.js +16 -8
  223. package/dist/internal/funding/Action.js.map +1 -1
  224. package/dist/internal/funding/Catalog.d.ts +9 -2
  225. package/dist/internal/funding/Catalog.d.ts.map +1 -1
  226. package/dist/internal/funding/Catalog.js +19 -8
  227. package/dist/internal/funding/Catalog.js.map +1 -1
  228. package/dist/internal/funding/DepositWebhook.d.ts.map +1 -1
  229. package/dist/internal/funding/DepositWebhook.js +2 -1
  230. package/dist/internal/funding/DepositWebhook.js.map +1 -1
  231. package/dist/internal/funding/Evidence.d.ts.map +1 -1
  232. package/dist/internal/funding/Evidence.js +2 -10
  233. package/dist/internal/funding/Evidence.js.map +1 -1
  234. package/dist/internal/funding/Provider.d.ts +108 -13
  235. package/dist/internal/funding/Provider.d.ts.map +1 -1
  236. package/dist/internal/funding/Provider.js +34 -8
  237. package/dist/internal/funding/Provider.js.map +1 -1
  238. package/dist/internal/funding/SourceObservation.d.ts.map +1 -1
  239. package/dist/internal/funding/SourceObservation.js +5 -7
  240. package/dist/internal/funding/SourceObservation.js.map +1 -1
  241. package/dist/internal/funding/Transfer.d.ts +114 -18
  242. package/dist/internal/funding/Transfer.d.ts.map +1 -1
  243. package/dist/internal/funding/Transfer.js +185 -55
  244. package/dist/internal/funding/Transfer.js.map +1 -1
  245. package/dist/internal/funding/TransferReconciliation.d.ts +52 -0
  246. package/dist/internal/funding/TransferReconciliation.d.ts.map +1 -0
  247. package/dist/internal/funding/TransferReconciliation.js +192 -0
  248. package/dist/internal/funding/TransferReconciliation.js.map +1 -0
  249. package/dist/internal/funding/TransferWebhook.d.ts +21 -0
  250. package/dist/internal/funding/TransferWebhook.d.ts.map +1 -0
  251. package/dist/internal/funding/TransferWebhook.js +43 -0
  252. package/dist/internal/funding/TransferWebhook.js.map +1 -0
  253. package/dist/internal/funding/index.d.ts +1 -0
  254. package/dist/internal/funding/index.d.ts.map +1 -1
  255. package/dist/internal/funding/index.js +1 -0
  256. package/dist/internal/funding/index.js.map +1 -1
  257. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  258. package/dist/internal/funding/providers/relay.js +3 -12
  259. package/dist/internal/funding/providers/relay.js.map +1 -1
  260. package/dist/internal/funding/providers/stargate.d.ts +127 -9
  261. package/dist/internal/funding/providers/stargate.d.ts.map +1 -1
  262. package/dist/internal/funding/providers/stargate.js +548 -112
  263. package/dist/internal/funding/providers/stargate.js.map +1 -1
  264. package/dist/internal/index.d.ts +1 -0
  265. package/dist/internal/index.d.ts.map +1 -1
  266. package/dist/internal/index.js +1 -0
  267. package/dist/internal/index.js.map +1 -1
  268. package/dist/internal/webhookDestinations/slack.d.ts.map +1 -1
  269. package/dist/internal/webhookDestinations/slack.js +16 -0
  270. package/dist/internal/webhookDestinations/slack.js.map +1 -1
  271. package/package.json +2 -2
  272. package/src/App.test.ts +100 -0
  273. package/src/App.ts +43 -3
  274. package/src/Client.test-d.ts +129 -2
  275. package/src/Scope.test.ts +20 -0
  276. package/src/Scope.ts +16 -0
  277. package/src/Webhooks.ts +2 -2
  278. package/src/admin/apps/api-keys.test.ts +75 -0
  279. package/src/admin/apps/api-keys.ts +64 -0
  280. package/src/admin/apps/earn-vaults.test.ts +1 -1
  281. package/src/admin/apps/earn-vaults.ts +2 -1
  282. package/src/admin/apps/organizations.test.ts +53 -1
  283. package/src/admin/apps/organizations.ts +90 -5
  284. package/src/admin/ui/src/lib/api-keys.ts +19 -0
  285. package/src/admin/ui/src/lib/organizations.test.ts +112 -1
  286. package/src/admin/ui/src/lib/organizations.ts +55 -0
  287. package/src/admin/ui/src/routes/api-keys.tsx +89 -2
  288. package/src/admin/ui/src/routes/organizations_.$orgId.tsx +116 -0
  289. package/src/admin/ui.generated.ts +189 -151
  290. package/src/apps/Zones.test.ts +7 -7
  291. package/src/apps/Zones.ts +7 -7
  292. package/src/apps/composition.test.ts +2 -1
  293. package/src/apps/data/App.test.ts +41 -0
  294. package/src/apps/data/App.ts +30 -21
  295. package/src/apps/data/ZoneSelection.test.ts +2 -2
  296. package/src/apps/data/ZoneSelection.ts +8 -8
  297. package/src/apps/data/routes/activities.test.ts +1526 -12
  298. package/src/apps/data/routes/activities.ts +1774 -1356
  299. package/src/apps/data/routes/balances.ts +133 -97
  300. package/src/apps/data/routes/blocks.test.ts +533 -0
  301. package/src/apps/data/routes/blocks.ts +35 -16
  302. package/src/apps/data/routes/earn.test.ts +373 -16
  303. package/src/apps/data/routes/earn.ts +548 -79
  304. package/src/apps/data/routes/earn.zone-access.test.ts +6 -6
  305. package/src/apps/data/routes/exchanges.test.ts +740 -15
  306. package/src/apps/data/routes/exchanges.ts +954 -753
  307. package/src/apps/data/routes/fee-amm.ts +17 -18
  308. package/src/apps/data/routes/receipts.ts +47 -16
  309. package/src/apps/data/routes/tokens.test.ts +578 -0
  310. package/src/apps/data/routes/tokens.ts +283 -225
  311. package/src/apps/data/routes/transactions.ts +68 -29
  312. package/src/apps/data/routes/transfers.test.ts +421 -0
  313. package/src/apps/data/routes/transfers.ts +45 -27
  314. package/src/apps/data/routes/valuation.ts +94 -78
  315. package/src/apps/data/routes/webhooks.test.ts +21 -5
  316. package/src/apps/data/routes/webhooks.ts +63 -8
  317. package/src/apps/data/routes/zones.test.ts +2 -2
  318. package/src/apps/data/routes/zones.ts +1 -1
  319. package/src/apps/funding/App.ts +12 -2
  320. package/src/apps/funding/routes/deposit-addresses.test.ts +29 -7
  321. package/src/apps/funding/routes/deposit-addresses.ts +19 -14
  322. package/src/apps/funding/routes/quotes.test.ts +5 -9
  323. package/src/apps/funding/routes/transfers.test.ts +901 -23
  324. package/src/apps/funding/routes/transfers.ts +347 -64
  325. package/src/apps/management/Billing.ts +18 -5
  326. package/src/apps/management/routes/api-keys.test.ts +220 -0
  327. package/src/apps/management/routes/api-keys.ts +280 -3
  328. package/src/apps/management/routes/webhooks.test.ts +3 -3
  329. package/src/apps/management/routes/webhooks.ts +24 -25
  330. package/src/apps/relay/App.test.ts +212 -1
  331. package/src/apps/relay/App.ts +158 -29
  332. package/src/apps/relay/Sponsorships.ts +24 -1
  333. package/src/db/Db.ts +48 -1
  334. package/src/db/Schema.ts +22 -4
  335. package/src/db/tables/earnVaults.test.ts +9 -9
  336. package/src/db/tables/fundingCatalog.test.ts +16 -2
  337. package/src/db/tables/fundingCatalog.ts +9 -3
  338. package/src/db/tables/fundingTransferTransactions.test.ts +43 -0
  339. package/src/db/tables/fundingTransferTransactions.ts +79 -3
  340. package/src/db/tables/fundingTransfers.test.ts +26 -0
  341. package/src/db/tables/fundingTransfers.ts +36 -0
  342. package/src/db/tables/organizations.test.ts +41 -0
  343. package/src/db/tables/organizations.ts +49 -0
  344. package/src/db/tables/projects.test.ts +55 -0
  345. package/src/db/tables/projects.ts +46 -1
  346. package/src/db/tables/sponsoredTransactions.test.ts +338 -0
  347. package/src/db/tables/sponsoredTransactions.ts +197 -13
  348. package/src/db/tables/webhookSubscriptions.ts +18 -11
  349. package/src/handlers/internal/utils.test.ts +53 -0
  350. package/src/handlers/internal/utils.ts +15 -0
  351. package/src/internal/Auth.ts +12 -4
  352. package/src/internal/EarnVaults.capabilities.test.ts +2 -2
  353. package/src/internal/EarnVaults.test.ts +1 -1
  354. package/src/internal/EarnVaults.ts +5 -5
  355. package/src/internal/Fetch.test.ts +46 -0
  356. package/src/internal/Fetch.ts +56 -0
  357. package/src/internal/Log.test-d.ts +2 -0
  358. package/src/internal/Log.test.ts +21 -0
  359. package/src/internal/Log.ts +22 -5
  360. package/src/internal/OpenApi.ts +14 -4
  361. package/src/internal/Response.ts +1 -1
  362. package/src/internal/Schema.test.ts +2 -0
  363. package/src/internal/Schema.ts +12 -2
  364. package/src/internal/Store.test.ts +178 -0
  365. package/src/internal/Store.ts +104 -18
  366. package/src/internal/Tidx.test.ts +2 -1
  367. package/src/internal/Tidx.ts +7 -2
  368. package/src/internal/Timeout.test.ts +35 -0
  369. package/src/internal/Timeout.ts +73 -0
  370. package/src/internal/Timing.ts +16 -0
  371. package/src/internal/Viem.ts +39 -3
  372. package/src/internal/WebhookDestination.ts +3 -0
  373. package/src/internal/Webhooks.test.ts +45 -0
  374. package/src/internal/funding/Action.test.ts +41 -1
  375. package/src/internal/funding/Action.ts +69 -41
  376. package/src/internal/funding/Catalog.test.ts +34 -0
  377. package/src/internal/funding/Catalog.ts +28 -11
  378. package/src/internal/funding/DepositWebhook.ts +2 -1
  379. package/src/internal/funding/Evidence.ts +2 -13
  380. package/src/internal/funding/Provider.test.ts +73 -0
  381. package/src/internal/funding/Provider.ts +125 -28
  382. package/src/internal/funding/SourceObservation.ts +5 -9
  383. package/src/internal/funding/Transfer.test.ts +155 -0
  384. package/src/internal/funding/Transfer.ts +387 -147
  385. package/src/internal/funding/TransferReconciliation.test.ts +147 -0
  386. package/src/internal/funding/TransferReconciliation.ts +264 -0
  387. package/src/internal/funding/TransferWebhook.test.ts +128 -0
  388. package/src/internal/funding/TransferWebhook.ts +67 -0
  389. package/src/internal/funding/index.test.ts +1 -0
  390. package/src/internal/funding/index.ts +1 -0
  391. package/src/internal/funding/providers/index.test.ts +4 -167
  392. package/src/internal/funding/providers/relay.test.ts +2 -2
  393. package/src/internal/funding/providers/relay.ts +3 -15
  394. package/src/internal/funding/providers/stargate.test.ts +328 -0
  395. package/src/internal/funding/providers/stargate.ts +807 -135
  396. package/src/internal/index.test.ts +1 -0
  397. package/src/internal/index.ts +1 -0
  398. package/src/internal/webhookDestinations/slack.ts +15 -0
@@ -3,6 +3,7 @@ import { nanoid } from 'nanoid'
3
3
  import * as Runtime from '../../../test/runtime.js'
4
4
  import * as Db from '../Db.js'
5
5
  import * as Organizations from './organizations.js'
6
+ import * as Projects from './projects.js'
6
7
  import * as SponsoredTransactions from './sponsoredTransactions.js'
7
8
 
8
9
  const create = () => Db.postgres({ connectionString: Runtime.postgresUrl, schema: `t_${nanoid()}` })
@@ -124,6 +125,20 @@ describe('upsert', () => {
124
125
  await db.close()
125
126
  })
126
127
 
128
+ test('behavior: rejects a sign payload already attributed to another project', async () => {
129
+ const db = create()
130
+ await db.migrate()
131
+ const { transactionHash: _, ...intent } = input
132
+ const first = await SponsoredTransactions.upsert(db, intent)
133
+
134
+ await expect(
135
+ SponsoredTransactions.upsert(db, { ...input, projectId: 'prj_2' }),
136
+ ).rejects.toBeInstanceOf(SponsoredTransactions.AttributionConflictError)
137
+
138
+ expect(await SponsoredTransactions.get(db, first.id)).toStrictEqual(first)
139
+ await db.close()
140
+ })
141
+
127
142
  test('behavior: a raw submission reopens a failed fill intent', async () => {
128
143
  const db = create()
129
144
  await db.migrate()
@@ -393,6 +408,320 @@ describe('spend', () => {
393
408
  expect(await SponsoredTransactions.spend(db, { chainIds: [input.chainId], orgId: 'org_1', since: future })).toBe(0n) // prettier-ignore
394
409
  await db.close()
395
410
  })
411
+
412
+ test('behavior: promotional spend is isolated by project', async () => {
413
+ const db = create()
414
+ await db.migrate()
415
+ await SponsoredTransactions.upsert(db, {
416
+ ...input,
417
+ billable: false,
418
+ feeMax: '100',
419
+ projectId: 'prj_1',
420
+ signPayload: `0x${'09'.repeat(32)}`,
421
+ })
422
+ await SponsoredTransactions.upsert(db, {
423
+ ...input,
424
+ billable: false,
425
+ feeMax: '200',
426
+ projectId: 'prj_2',
427
+ signPayload: `0x${'0a'.repeat(32)}`,
428
+ })
429
+
430
+ expect(
431
+ await SponsoredTransactions.spend(db, {
432
+ billable: false,
433
+ chainIds: [input.chainId],
434
+ orgId: 'org_1',
435
+ projectId: 'prj_1',
436
+ since: '2000-01-01T00:00:00.000Z',
437
+ }),
438
+ ).toBe(100n)
439
+ await db.close()
440
+ })
441
+ })
442
+
443
+ describe('reserve', () => {
444
+ test('behavior: enforces promotional limits independently per project', async () => {
445
+ const db = create()
446
+ await db.migrate()
447
+ const limit = {
448
+ billable: false,
449
+ chainIds: [input.chainId],
450
+ max: 100n,
451
+ projectId: 'prj_1',
452
+ since: '2000-01-01T00:00:00.000Z',
453
+ } as const
454
+ await SponsoredTransactions.reserve(
455
+ db,
456
+ {
457
+ ...input,
458
+ billable: false,
459
+ feeMax: '100',
460
+ signPayload: `0x${'0b'.repeat(32)}`,
461
+ },
462
+ limit,
463
+ )
464
+ await SponsoredTransactions.reserve(
465
+ db,
466
+ {
467
+ ...input,
468
+ billable: false,
469
+ feeMax: '100',
470
+ projectId: 'prj_2',
471
+ signPayload: `0x${'0c'.repeat(32)}`,
472
+ },
473
+ { ...limit, projectId: 'prj_2' },
474
+ )
475
+
476
+ await expect(
477
+ SponsoredTransactions.reserve(
478
+ db,
479
+ {
480
+ ...input,
481
+ billable: false,
482
+ feeMax: '1',
483
+ signPayload: `0x${'0d'.repeat(32)}`,
484
+ },
485
+ limit,
486
+ ),
487
+ ).rejects.toBeInstanceOf(SponsoredTransactions.PeriodSpendLimitError)
488
+ await db.close()
489
+ })
490
+ })
491
+
492
+ describe('reservePromotion', () => {
493
+ async function configure(db: Db.Db, projectSpendLimit: string | null) {
494
+ await Organizations.create(db, { id: 'org_1', name: 'Privy' })
495
+ await Organizations.setSponsorshipSubsidy(db, 'org_1', {
496
+ durationDays: 90,
497
+ projectSpendLimit,
498
+ })
499
+ }
500
+
501
+ test('behavior: atomically records the first sponsorship and immutable window', async () => {
502
+ const db = create()
503
+ await db.migrate()
504
+ await configure(db, '0.000001')
505
+ const project = await Projects.create(db, { name: 'App', orgId: 'org_1' })
506
+ const result = await SponsoredTransactions.reservePromotion(
507
+ db,
508
+ { ...input, billable: false, projectId: project.id },
509
+ {
510
+ at: '2026-01-01T00:00:00.000Z',
511
+ chainIds: [input.chainId],
512
+ projectId: project.id,
513
+ },
514
+ )
515
+ const updated = await Projects.get(db, project.id)
516
+ expect({
517
+ endsAt: updated?.sponsorshipSubsidyEndsAt,
518
+ startsAt: updated?.sponsorshipSubsidyStartsAt,
519
+ }).toMatchInlineSnapshot(`
520
+ {
521
+ "endsAt": "2026-04-01T00:00:00.000Z",
522
+ "startsAt": "2026-01-01T00:00:00.000Z",
523
+ }
524
+ `)
525
+ if (result.status === 'ineligible') throw new Error('Expected a promotion reservation.')
526
+ expect(result.record.billable).toBe(false)
527
+ expect((await Projects.get(db, project.id))?.sponsorshipSpendLimit).toBe('1')
528
+ await db.close()
529
+ })
530
+
531
+ test('behavior: records an uncapped promotion', async () => {
532
+ const db = create()
533
+ await db.migrate()
534
+ await configure(db, null)
535
+ const project = await Projects.create(db, { name: 'App', orgId: 'org_1' })
536
+ const result = await SponsoredTransactions.reservePromotion(
537
+ db,
538
+ { ...input, billable: false, feeMax: '100', projectId: project.id },
539
+ {
540
+ at: '2026-01-01T00:00:00.000Z',
541
+ chainIds: [input.chainId],
542
+ projectId: project.id,
543
+ },
544
+ )
545
+ if (result.status === 'ineligible') throw new Error('Expected a promotion reservation.')
546
+ expect(result.record.billable).toBe(false)
547
+ expect(await Projects.get(db, project.id)).toMatchObject({
548
+ sponsorshipSubsidyEndsAt: '2026-04-01T00:00:00.000Z',
549
+ sponsorshipSubsidyStartsAt: '2026-01-01T00:00:00.000Z',
550
+ sponsorshipSpendLimit: null,
551
+ })
552
+ await SponsoredTransactions.reservePromotion(
553
+ db,
554
+ {
555
+ ...input,
556
+ billable: false,
557
+ feeMax: '100',
558
+ projectId: project.id,
559
+ signPayload: `0x${'24'.repeat(32)}`,
560
+ },
561
+ {
562
+ at: '2026-01-02T00:00:00.000Z',
563
+ chainIds: [input.chainId],
564
+ projectId: project.id,
565
+ },
566
+ )
567
+ expect((await Projects.get(db, project.id))?.sponsorshipSpendLimit).toBeNull()
568
+ await db.close()
569
+ })
570
+
571
+ test('behavior: reads the current organization policy when activating', async () => {
572
+ const db = create()
573
+ await db.migrate()
574
+ await configure(db, '0.000001')
575
+ const project = await Projects.create(db, { name: 'App', orgId: 'org_1' })
576
+ await Organizations.setSponsorshipSubsidy(db, 'org_1', {
577
+ durationDays: 90,
578
+ projectSpendLimit: '0.000002',
579
+ })
580
+
581
+ const result = await SponsoredTransactions.reservePromotion(
582
+ db,
583
+ { ...input, billable: false, feeMax: '2', projectId: project.id },
584
+ {
585
+ at: '2026-01-01T00:00:00.000Z',
586
+ chainIds: [input.chainId],
587
+ projectId: project.id,
588
+ },
589
+ )
590
+
591
+ expect(result.status).toBe('subsidized')
592
+ expect((await Projects.get(db, project.id))?.sponsorshipSpendLimit).toBe('2')
593
+ await db.close()
594
+ })
595
+
596
+ test('behavior: conflicting envelopes do not activate another project', async () => {
597
+ const db = create()
598
+ await db.migrate()
599
+ await configure(db, null)
600
+ const first = await Projects.create(db, { name: 'First', orgId: 'org_1' })
601
+ const second = await Projects.create(db, { name: 'Second', orgId: 'org_1' })
602
+ await SponsoredTransactions.upsert(db, { ...input, projectId: first.id })
603
+
604
+ await expect(
605
+ SponsoredTransactions.reservePromotion(
606
+ db,
607
+ { ...input, billable: false, projectId: second.id },
608
+ {
609
+ at: '2026-01-01T00:00:00.000Z',
610
+ chainIds: [input.chainId],
611
+ projectId: second.id,
612
+ },
613
+ ),
614
+ ).rejects.toBeInstanceOf(SponsoredTransactions.AttributionConflictError)
615
+
616
+ expect(await Projects.get(db, second.id)).toMatchObject({
617
+ sponsorshipSubsidyEndsAt: null,
618
+ sponsorshipSubsidyStartsAt: null,
619
+ })
620
+ await db.close()
621
+ })
622
+
623
+ test('behavior: later policy changes cannot raise an activated project cap', async () => {
624
+ const db = create()
625
+ await db.migrate()
626
+ await configure(db, '0.000001')
627
+ const project = await Projects.create(db, { name: 'App', orgId: 'org_1' })
628
+ const promotion = {
629
+ at: '2026-01-01T00:00:00.000Z',
630
+ chainIds: [input.chainId],
631
+ projectId: project.id,
632
+ } as const
633
+ await SponsoredTransactions.reservePromotion(
634
+ db,
635
+ {
636
+ ...input,
637
+ billable: false,
638
+ feeMax: '1',
639
+ projectId: project.id,
640
+ },
641
+ promotion,
642
+ )
643
+ await Organizations.setSponsorshipSubsidy(db, 'org_1', {
644
+ durationDays: 90,
645
+ projectSpendLimit: '0.000002',
646
+ })
647
+
648
+ const result = await SponsoredTransactions.reservePromotion(
649
+ db,
650
+ {
651
+ ...input,
652
+ billable: false,
653
+ feeMax: '1',
654
+ projectId: project.id,
655
+ signPayload: `0x${'23'.repeat(32)}`,
656
+ },
657
+ promotion,
658
+ )
659
+ if (result.status === 'ineligible') throw new Error('Expected a promotion reservation.')
660
+ expect(result.record.billable).toBe(true)
661
+ expect((await Projects.get(db, project.id))?.sponsorshipSpendLimit).toBe('1')
662
+ await db.close()
663
+ })
664
+
665
+ test('behavior: preserves activation when the first sponsorship exceeds the cap', async () => {
666
+ const db = create()
667
+ await db.migrate()
668
+ await configure(db, '0')
669
+ const project = await Projects.create(db, { name: 'App', orgId: 'org_1' })
670
+ const result = await SponsoredTransactions.reservePromotion(
671
+ db,
672
+ { ...input, billable: false, feeMax: '1', projectId: project.id },
673
+ {
674
+ at: '2026-01-01T00:00:00.000Z',
675
+ chainIds: [input.chainId],
676
+ projectId: project.id,
677
+ },
678
+ )
679
+ if (result.status === 'ineligible') throw new Error('Expected a promotion reservation.')
680
+ expect(result.record.billable).toBe(true)
681
+ expect(await Projects.get(db, project.id)).toMatchObject({
682
+ sponsorshipSubsidyEndsAt: '2026-04-01T00:00:00.000Z',
683
+ sponsorshipSubsidyStartsAt: '2026-01-01T00:00:00.000Z',
684
+ })
685
+ expect(await SponsoredTransactions.listPending(db)).toMatchObject([{ billable: true }])
686
+ await db.close()
687
+ })
688
+
689
+ test('behavior: concurrent first sponsorships share one promotion window', async () => {
690
+ const schema = `t_${nanoid()}`
691
+ const firstDb = Db.postgres({ connectionString: Runtime.postgresUrl, schema })
692
+ const secondDb = Db.postgres({ connectionString: Runtime.postgresUrl, schema })
693
+ await firstDb.migrate()
694
+ await configure(firstDb, '0.000002')
695
+ const project = await Projects.create(firstDb, { name: 'App', orgId: 'org_1' })
696
+ const reserve = (db: Db.Db, at: string, signPayload: string) =>
697
+ SponsoredTransactions.reservePromotion(
698
+ db,
699
+ {
700
+ ...input,
701
+ billable: false,
702
+ projectId: project.id,
703
+ signPayload,
704
+ },
705
+ {
706
+ at,
707
+ chainIds: [input.chainId],
708
+ projectId: project.id,
709
+ },
710
+ )
711
+ const [first, second] = await Promise.all([
712
+ reserve(firstDb, '2026-01-01T00:00:00.000Z', `0x${'21'.repeat(32)}`),
713
+ reserve(secondDb, '2026-01-02T00:00:00.000Z', `0x${'22'.repeat(32)}`),
714
+ ])
715
+ expect(first.status).toBe('subsidized')
716
+ expect(second.status).toBe('subsidized')
717
+ const updated = await Projects.get(firstDb, project.id)
718
+ if (!updated?.sponsorshipSubsidyEndsAt || !updated.sponsorshipSubsidyStartsAt)
719
+ throw new Error('Expected a promotion window.')
720
+ expect(
721
+ Date.parse(updated.sponsorshipSubsidyEndsAt) - Date.parse(updated.sponsorshipSubsidyStartsAt),
722
+ ).toBe(90 * 86_400_000)
723
+ await Promise.all([firstDb.close(), secondDb.close()])
724
+ })
396
725
  })
397
726
 
398
727
  describe('listUnreported', () => {
@@ -407,6 +736,15 @@ describe('listUnreported', () => {
407
736
  // Pending, sandbox, and foreign-chain rows never report.
408
737
  await SponsoredTransactions.upsert(db, { ...input, signPayload: `0x${'13'.repeat(32)}` })
409
738
  await SponsoredTransactions.upsert(db, { ...input, billable: false, environment: 'sandbox', signPayload: `0x${'14'.repeat(32)}` }) // prettier-ignore
739
+ const promotion = await SponsoredTransactions.upsert(db, {
740
+ ...input,
741
+ billable: false,
742
+ signPayload: `0x${'16'.repeat(32)}`,
743
+ })
744
+ await SponsoredTransactions.finalize(db, promotion.id, {
745
+ feeAmount: '300',
746
+ finalizedAt: '2026-01-03T00:00:00.000Z',
747
+ })
410
748
 
411
749
  const rows = await SponsoredTransactions.listUnreported(db, { chainIds })
412
750
  expect(rows.map((row) => row.id)).toStrictEqual([first.id, second.id])
@@ -1,9 +1,12 @@
1
1
  import type { ExpressionBuilder, Selectable } from 'kysely'
2
2
  import { sql } from 'kysely'
3
+ import { Value } from 'ox'
3
4
 
5
+ import * as Fees from '../../internal/Fees.js'
4
6
  import * as Id from '../../internal/Id.js'
5
7
  import type * as Db from '../Db.js'
6
8
  import type * as db_Schema from '../Schema.js'
9
+ import * as Projects from './projects.js'
7
10
 
8
11
  /** Columns of the `sponsored_transactions` table, derived from `Schema.SponsoredTransaction`. */
9
12
  export type Table = db_Schema.SponsoredTransaction
@@ -46,7 +49,7 @@ export async function upsert(db: Db.Db, input: upsert.Input): Promise<Record> {
46
49
  transaction: input.transaction,
47
50
  transactionHash: input.transactionHash ?? null,
48
51
  }
49
- return await db.kysely
52
+ const stored = await db.kysely
50
53
  .insertInto('sponsored_transactions')
51
54
  .values(record)
52
55
  .onConflict((oc) =>
@@ -69,11 +72,16 @@ export async function upsert(db: Db.Db, input: upsert.Input): Promise<Record> {
69
72
  finalizedAt: sql`CASE WHEN ${eb.ref('sponsored_transactions.status')} = 'failed' THEN NULL ELSE ${eb.ref('sponsored_transactions.finalizedAt')} END`,
70
73
  status: sql`CASE WHEN ${eb.ref('sponsored_transactions.status')} = 'failed' THEN 'pending' ELSE ${eb.ref('sponsored_transactions.status')} END`,
71
74
  }))
72
- // A shared envelope cannot transfer billing attribution across tenants.
73
- .where('sponsored_transactions.orgId', '=', input.orgId),
75
+ // A shared envelope cannot transfer billing attribution between owners.
76
+ .where('sponsored_transactions.orgId', '=', input.orgId)
77
+ .where(
78
+ sql<boolean>`sponsored_transactions.project_id IS NOT DISTINCT FROM ${record.projectId}`,
79
+ ),
74
80
  )
75
81
  .returningAll()
76
- .executeTakeFirstOrThrow()
82
+ .executeTakeFirst()
83
+ if (!stored) throw new AttributionConflictError()
84
+ return stored
77
85
  }
78
86
 
79
87
  export declare namespace upsert {
@@ -208,8 +216,13 @@ export async function assignTransactionHash(
208
216
  (record.finalizedAt === null || record.finalizedAt < failedSince)
209
217
  )
210
218
  return
211
- if (record.status === 'failed' && record.billable && limit)
212
- await sql`select pg_advisory_xact_lock(${billingLockClass}, hashtext(${record.orgId}))`.execute(tx.kysely) // prettier-ignore
219
+ const limited =
220
+ limit !== undefined &&
221
+ (limit.billable === undefined ? record.billable : record.billable === limit.billable)
222
+ if (record.status === 'failed' && limited) {
223
+ const scope = limit.projectId ? `${record.orgId}:${limit.projectId}` : record.orgId
224
+ await sql`select pg_advisory_xact_lock(${billingLockClass}, hashtext(${scope}))`.execute(tx.kysely) // prettier-ignore
225
+ }
213
226
  let query = tx.kysely
214
227
  .updateTable('sponsored_transactions')
215
228
  .set({
@@ -232,10 +245,12 @@ export async function assignTransactionHash(
232
245
  )
233
246
  const updated = await query.executeTakeFirst()
234
247
  if (updated.numUpdatedRows === 0n) return
235
- if (record.status === 'failed' && record.billable && limit) {
248
+ if (record.status === 'failed' && limited) {
236
249
  const committed = await spend(tx, {
250
+ ...(limit.billable === undefined ? {} : { billable: limit.billable }),
237
251
  chainIds: limit.chainIds,
238
252
  orgId: record.orgId,
253
+ ...(limit.projectId === undefined ? {} : { projectId: limit.projectId }),
239
254
  since: limit.since,
240
255
  })
241
256
  if (committed > limit.max) throw new PeriodSpendLimitError()
@@ -319,12 +334,17 @@ export async function reserve(
319
334
  return db.transaction(async (tx) => {
320
335
  // `hashtext` maps the org id into the advisory-lock keyspace; the class
321
336
  // namespaces it against other advisory-lock users. Released at tx end.
322
- await sql`select pg_advisory_xact_lock(${billingLockClass}, hashtext(${input.orgId}))`.execute(tx.kysely) // prettier-ignore
337
+ const scope = limit.projectId ? `${input.orgId}:${limit.projectId}` : input.orgId
338
+ await sql`select pg_advisory_xact_lock(${billingLockClass}, hashtext(${scope}))`.execute(tx.kysely) // prettier-ignore
323
339
  const record = await upsert(tx, input)
324
- if (record.billable) {
340
+ const limited =
341
+ limit.billable === undefined ? record.billable : record.billable === limit.billable
342
+ if (limited) {
325
343
  const committed = await spend(tx, {
344
+ ...(limit.billable === undefined ? {} : { billable: limit.billable }),
326
345
  chainIds: limit.chainIds,
327
346
  orgId: input.orgId,
347
+ ...(limit.projectId === undefined ? {} : { projectId: limit.projectId }),
328
348
  since: limit.since,
329
349
  })
330
350
  if (committed > limit.max) throw new PeriodSpendLimitError()
@@ -339,15 +359,160 @@ const billingLockClass = 1112294220
339
359
  export declare namespace reserve {
340
360
  /** The period spend limit enforced by {@link reserve}. */
341
361
  type Limit = {
362
+ /** Billing snapshot whose rows consume this limit; omit for customer-billable spend. */
363
+ billable?: boolean | undefined
342
364
  /** Chains whose spend counts (mainnet chain ids). */
343
365
  chainIds: readonly number[]
344
366
  /** Limit in fee-token base units. */
345
367
  max: bigint
368
+ /** Project scope for independent limits; omit for the organization-wide budget. */
369
+ projectId?: string | undefined
346
370
  /** Window start (ISO 8601). */
347
371
  since: string
348
372
  }
349
373
  }
350
374
 
375
+ /**
376
+ * Applies the current promotion policy and records the sponsorship atomically.
377
+ * Exhausted promotions retain their window and fall back to customer billing.
378
+ */
379
+ export function reservePromotion(
380
+ db: Db.Db,
381
+ input: upsert.Input,
382
+ options: reservePromotion.Options,
383
+ ): Promise<reservePromotion.Result> {
384
+ return db.transaction(async (tx) => {
385
+ const scope = `${input.orgId}:${options.projectId}`
386
+ await sql`select pg_advisory_xact_lock(${billingLockClass}, hashtext(${scope}))`.execute(tx.kysely) // prettier-ignore
387
+ const existing = await tx.kysely
388
+ .selectFrom('sponsored_transactions')
389
+ .selectAll()
390
+ .where('signPayload', '=', input.signPayload)
391
+ .forUpdate()
392
+ .executeTakeFirst()
393
+ if (existing) {
394
+ if (existing.orgId !== input.orgId || existing.projectId !== options.projectId)
395
+ throw new AttributionConflictError()
396
+ const stored = await upsert(tx, input)
397
+ if (stored.billable && options.billingLimit) {
398
+ await sql`select pg_advisory_xact_lock(${billingLockClass}, hashtext(${input.orgId}))`.execute(tx.kysely) // prettier-ignore
399
+ const billed = await spend(tx, {
400
+ chainIds: options.billingLimit.chainIds,
401
+ orgId: input.orgId,
402
+ since: options.billingLimit.since,
403
+ })
404
+ if (billed > options.billingLimit.max) throw new PeriodSpendLimitError()
405
+ }
406
+ if (!stored.billable) {
407
+ const project = await Projects.get(tx, options.projectId)
408
+ if (!project?.sponsorshipSubsidyStartsAt) throw new PromotionActivationError()
409
+ if (project.sponsorshipSpendLimit !== null) {
410
+ const committed = await spend(tx, {
411
+ billable: false,
412
+ chainIds: options.chainIds,
413
+ orgId: input.orgId,
414
+ projectId: options.projectId,
415
+ since: project.sponsorshipSubsidyStartsAt,
416
+ })
417
+ if (committed > BigInt(project.sponsorshipSpendLimit)) throw new PeriodSpendLimitError()
418
+ }
419
+ }
420
+ return { record: stored, status: stored.billable ? 'billed' : 'subsidized' }
421
+ }
422
+
423
+ const organization = await tx.kysely
424
+ .selectFrom('organizations')
425
+ .select(['sponsorshipSubsidyDurationDays', 'sponsorshipSubsidyProjectSpendLimit'])
426
+ .where('id', '=', input.orgId)
427
+ .forShare()
428
+ .executeTakeFirst()
429
+ if (!organization || organization.sponsorshipSubsidyDurationDays === null)
430
+ return { status: 'ineligible' }
431
+
432
+ const current = await Projects.get(tx, options.projectId)
433
+ if (!current || current.orgId !== input.orgId) throw new PromotionActivationError()
434
+ if (
435
+ current.sponsorshipSubsidyEndsAt !== null &&
436
+ Date.parse(options.at) >= Date.parse(current.sponsorshipSubsidyEndsAt)
437
+ )
438
+ return { status: 'ineligible' }
439
+
440
+ const endsAt = new Date(
441
+ Date.parse(options.at) + organization.sponsorshipSubsidyDurationDays * 86_400_000,
442
+ ).toISOString()
443
+ const project = await Projects.activateSponsorship(tx, {
444
+ at: options.at,
445
+ endsAt,
446
+ id: options.projectId,
447
+ orgId: input.orgId,
448
+ spendLimit:
449
+ organization.sponsorshipSubsidyProjectSpendLimit === null
450
+ ? null
451
+ : Value.from(
452
+ organization.sponsorshipSubsidyProjectSpendLimit,
453
+ Fees.tokenDecimals,
454
+ ).toString(),
455
+ })
456
+ if (!project?.sponsorshipSubsidyStartsAt || !project.sponsorshipSubsidyEndsAt)
457
+ throw new PromotionActivationError()
458
+ let record = await upsert(tx, input)
459
+ if (project.sponsorshipSpendLimit !== null) {
460
+ const committed = await spend(tx, {
461
+ billable: false,
462
+ chainIds: options.chainIds,
463
+ orgId: input.orgId,
464
+ projectId: options.projectId,
465
+ since: project.sponsorshipSubsidyStartsAt,
466
+ })
467
+ if (committed > BigInt(project.sponsorshipSpendLimit)) {
468
+ record = await tx.kysely
469
+ .updateTable('sponsored_transactions')
470
+ .set({ billable: true })
471
+ .where('id', '=', record.id)
472
+ .returningAll()
473
+ .executeTakeFirstOrThrow()
474
+ if (options.billingLimit) {
475
+ await sql`select pg_advisory_xact_lock(${billingLockClass}, hashtext(${input.orgId}))`.execute(tx.kysely) // prettier-ignore
476
+ const billed = await spend(tx, {
477
+ chainIds: options.billingLimit.chainIds,
478
+ orgId: input.orgId,
479
+ since: options.billingLimit.since,
480
+ })
481
+ if (billed > options.billingLimit.max) throw new PeriodSpendLimitError()
482
+ }
483
+ }
484
+ }
485
+ return { record, status: record.billable ? 'billed' : 'subsidized' }
486
+ })
487
+ }
488
+
489
+ export declare namespace reservePromotion {
490
+ /** Project promotion window and cap. */
491
+ type Options = {
492
+ /** Timestamp of the recorded sponsorship that may activate the window. */
493
+ at: string
494
+ /** Customer billing limit used when the project promotion is exhausted. */
495
+ billingLimit?: reserve.Limit | undefined
496
+ /** Mainnet chains whose promotional spend counts. */
497
+ chainIds: readonly number[]
498
+ /** Project receiving the independent promotion. */
499
+ projectId: string
500
+ }
501
+
502
+ /** Promotion reservation outcome. */
503
+ type Result =
504
+ | {
505
+ /** Stored sponsorship. */
506
+ record: Record
507
+ /** Whether Tempo or the customer pays for the stored sponsorship. */
508
+ status: 'billed' | 'subsidized'
509
+ }
510
+ | {
511
+ /** The current organization or project policy does not grant a promotion. */
512
+ status: 'ineligible'
513
+ }
514
+ }
515
+
351
516
  /**
352
517
  * Billable spend committed since a cutoff, in fee-token base units: finalized
353
518
  * rows at their actual fee plus pending rows at their signed fee cap, so
@@ -358,8 +523,8 @@ export declare namespace reserve {
358
523
  * @returns The committed spend in base units.
359
524
  */
360
525
  export async function spend(db: Db.Db, options: spend.Options): Promise<bigint> {
361
- const { chainIds, environment = 'production', orgId, since } = options
362
- const query = db.kysely
526
+ const { billable, chainIds, environment = 'production', orgId, projectId, since } = options
527
+ let query = db.kysely
363
528
  .selectFrom('sponsored_transactions')
364
529
  .select([
365
530
  sql<string>`coalesce(sum(case when status = 'finalized' then fee_amount::numeric else 0 end), 0)`.as('finalized'), // prettier-ignore
@@ -372,22 +537,31 @@ export async function spend(db: Db.Db, options: spend.Options): Promise<bigint>
372
537
  .where('status', 'in', ['finalized', 'pending'])
373
538
  .where('chainId', 'in', [...chainIds])
374
539
  .where('createdAt', '>=', since)
540
+ if (billable !== undefined) query = query.where('billable', '=', billable)
541
+ if (projectId !== undefined) query = query.where('projectId', '=', projectId)
375
542
  // Production mirrors the sponsorship gate — only billable rows count. Sandbox
376
543
  // rows are non-billable by construction, so the figure is display-only.
377
- const row = await (environment === 'production' ? query.where('billable', '=', true) : query) //
378
- .executeTakeFirstOrThrow()
544
+ const row = await (
545
+ environment === 'production' && billable === undefined
546
+ ? query.where('billable', '=', true)
547
+ : query
548
+ ).executeTakeFirstOrThrow()
379
549
  return BigInt(row.finalized) + BigInt(row.pending)
380
550
  }
381
551
 
382
552
  export declare namespace spend {
383
553
  /** Options for {@link spend}. */
384
554
  type Options = {
555
+ /** Billing snapshot to count; omit for the existing production-billable behavior. */
556
+ billable?: boolean | undefined
385
557
  /** Chains whose spend counts (mainnet chain ids). */
386
558
  chainIds: readonly number[]
387
559
  /** Environment whose spend counts; production mirrors the billing gate, sandbox is display-only. Defaults to `production`. */
388
560
  environment?: Table['environment'] | undefined
389
561
  /** Organization id (`org_…`) the spend attributes to. */
390
562
  orgId: string
563
+ /** Project whose spend counts; omit for organization-wide spend. */
564
+ projectId?: string | undefined
391
565
  /** Window start (ISO 8601); rows created earlier are out of scope. */
392
566
  since: string
393
567
  }
@@ -531,3 +705,13 @@ function intentStatus(failedSince: string | undefined) {
531
705
  export class PeriodSpendLimitError extends Error {
532
706
  override name = 'SponsoredTransactions.PeriodSpendLimitError'
533
707
  }
708
+
709
+ /** Thrown when a promotional window cannot be activated consistently. */
710
+ export class PromotionActivationError extends Error {
711
+ override name = 'SponsoredTransactions.PromotionActivationError'
712
+ }
713
+
714
+ /** Thrown when one signed envelope is presented with conflicting project attribution. */
715
+ export class AttributionConflictError extends Error {
716
+ override name = 'SponsoredTransactions.AttributionConflictError'
717
+ }