tapimo 0.13.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 (230) hide show
  1. package/dist/App.d.ts +6064 -4829
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +1 -0
  4. package/dist/App.js.map +1 -1
  5. package/dist/Webhooks.js +2 -2
  6. package/dist/Webhooks.js.map +1 -1
  7. package/dist/admin/App.d.ts +1008 -917
  8. package/dist/admin/App.d.ts.map +1 -1
  9. package/dist/admin/apps/earn-vaults.d.ts +48 -0
  10. package/dist/admin/apps/earn-vaults.d.ts.map +1 -1
  11. package/dist/admin/apps/earn-vaults.js +2 -1
  12. package/dist/admin/apps/earn-vaults.js.map +1 -1
  13. package/dist/admin/apps/organizations.d.ts +92 -3
  14. package/dist/admin/apps/organizations.d.ts.map +1 -1
  15. package/dist/admin/apps/organizations.js +69 -7
  16. package/dist/admin/apps/organizations.js.map +1 -1
  17. package/dist/admin/apps/verified-tokens.d.ts +74 -74
  18. package/dist/admin/ui.generated.d.ts +1 -1
  19. package/dist/admin/ui.generated.js +69 -69
  20. package/dist/admin/ui.generated.js.map +1 -1
  21. package/dist/apps/Zones.d.ts +12 -12
  22. package/dist/apps/Zones.js +3 -3
  23. package/dist/apps/data/App.d.ts +2909 -2523
  24. package/dist/apps/data/App.d.ts.map +1 -1
  25. package/dist/apps/data/App.js +11 -6
  26. package/dist/apps/data/App.js.map +1 -1
  27. package/dist/apps/data/routes/addresses.d.ts +22 -22
  28. package/dist/apps/data/routes/earn.d.ts +183 -7
  29. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  30. package/dist/apps/data/routes/earn.js +434 -75
  31. package/dist/apps/data/routes/earn.js.map +1 -1
  32. package/dist/apps/data/routes/exchanges.d.ts +15 -0
  33. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  34. package/dist/apps/data/routes/exchanges.js +180 -114
  35. package/dist/apps/data/routes/exchanges.js.map +1 -1
  36. package/dist/apps/data/routes/fee-amm.d.ts.map +1 -1
  37. package/dist/apps/data/routes/fee-amm.js +18 -19
  38. package/dist/apps/data/routes/fee-amm.js.map +1 -1
  39. package/dist/apps/data/routes/tokens.d.ts +93 -93
  40. package/dist/apps/data/routes/transfers.d.ts +12 -12
  41. package/dist/apps/data/routes/valuation.d.ts +30 -30
  42. package/dist/apps/data/routes/verified-tokens.d.ts +5 -5
  43. package/dist/apps/data/routes/webhooks.d.ts +805 -102
  44. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  45. package/dist/apps/data/routes/webhooks.js +47 -5
  46. package/dist/apps/data/routes/webhooks.js.map +1 -1
  47. package/dist/apps/funding/App.d.ts +456 -45
  48. package/dist/apps/funding/App.d.ts.map +1 -1
  49. package/dist/apps/funding/App.js +1 -1
  50. package/dist/apps/funding/App.js.map +1 -1
  51. package/dist/apps/funding/routes/deposit-addresses.d.ts +17 -17
  52. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
  53. package/dist/apps/funding/routes/deposit-addresses.js +1 -1
  54. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
  55. package/dist/apps/funding/routes/transfers.d.ts +654 -73
  56. package/dist/apps/funding/routes/transfers.d.ts.map +1 -1
  57. package/dist/apps/funding/routes/transfers.js +211 -5
  58. package/dist/apps/funding/routes/transfers.js.map +1 -1
  59. package/dist/apps/management/App.d.ts +831 -387
  60. package/dist/apps/management/App.d.ts.map +1 -1
  61. package/dist/apps/management/Billing.d.ts +1 -1
  62. package/dist/apps/management/Billing.d.ts.map +1 -1
  63. package/dist/apps/management/Billing.js +17 -1
  64. package/dist/apps/management/Billing.js.map +1 -1
  65. package/dist/apps/management/routes/api-keys.d.ts +76 -76
  66. package/dist/apps/management/routes/billing.d.ts +76 -76
  67. package/dist/apps/management/routes/faucet.d.ts +12 -12
  68. package/dist/apps/management/routes/invitations.d.ts +24 -24
  69. package/dist/apps/management/routes/invite-links.d.ts +46 -46
  70. package/dist/apps/management/routes/members.d.ts +46 -46
  71. package/dist/apps/management/routes/orgs.d.ts +20 -20
  72. package/dist/apps/management/routes/projects.d.ts +44 -44
  73. package/dist/apps/management/routes/usage.d.ts +32 -32
  74. package/dist/apps/management/routes/verified-token-requests.d.ts +16 -16
  75. package/dist/apps/management/routes/webhooks.d.ts +707 -57
  76. package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
  77. package/dist/apps/management/routes/webhooks.js +21 -21
  78. package/dist/apps/management/routes/webhooks.js.map +1 -1
  79. package/dist/apps/relay/App.d.ts.map +1 -1
  80. package/dist/apps/relay/App.js +115 -23
  81. package/dist/apps/relay/App.js.map +1 -1
  82. package/dist/apps/relay/Sponsorships.d.ts.map +1 -1
  83. package/dist/apps/relay/Sponsorships.js +27 -1
  84. package/dist/apps/relay/Sponsorships.js.map +1 -1
  85. package/dist/db/Db.d.ts.map +1 -1
  86. package/dist/db/Db.js +22 -0
  87. package/dist/db/Db.js.map +1 -1
  88. package/dist/db/Schema.d.ts +8 -3
  89. package/dist/db/Schema.d.ts.map +1 -1
  90. package/dist/db/Schema.js +16 -1
  91. package/dist/db/Schema.js.map +1 -1
  92. package/dist/db/tables/fundingTransferTransactions.d.ts +38 -0
  93. package/dist/db/tables/fundingTransferTransactions.d.ts.map +1 -1
  94. package/dist/db/tables/fundingTransferTransactions.js +44 -3
  95. package/dist/db/tables/fundingTransferTransactions.js.map +1 -1
  96. package/dist/db/tables/fundingTransfers.d.ts +17 -0
  97. package/dist/db/tables/fundingTransfers.d.ts.map +1 -1
  98. package/dist/db/tables/fundingTransfers.js +21 -0
  99. package/dist/db/tables/fundingTransfers.js.map +1 -1
  100. package/dist/db/tables/organizations.d.ts +24 -0
  101. package/dist/db/tables/organizations.d.ts.map +1 -1
  102. package/dist/db/tables/organizations.js +27 -0
  103. package/dist/db/tables/organizations.js.map +1 -1
  104. package/dist/db/tables/projects.d.ts +25 -1
  105. package/dist/db/tables/projects.d.ts.map +1 -1
  106. package/dist/db/tables/projects.js +26 -0
  107. package/dist/db/tables/projects.js.map +1 -1
  108. package/dist/db/tables/sponsoredTransactions.d.ts +44 -0
  109. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  110. package/dist/db/tables/sponsoredTransactions.js +146 -13
  111. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  112. package/dist/db/tables/webhookSubscriptions.d.ts +10 -6
  113. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  114. package/dist/db/tables/webhookSubscriptions.js +9 -6
  115. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  116. package/dist/internal/EarnVaults.js +5 -5
  117. package/dist/internal/EarnVaults.js.map +1 -1
  118. package/dist/internal/Log.d.ts +1 -1
  119. package/dist/internal/Log.d.ts.map +1 -1
  120. package/dist/internal/Log.js.map +1 -1
  121. package/dist/internal/Viem.d.ts +16 -1
  122. package/dist/internal/Viem.d.ts.map +1 -1
  123. package/dist/internal/Viem.js +12 -2
  124. package/dist/internal/Viem.js.map +1 -1
  125. package/dist/internal/funding/DepositWebhook.d.ts.map +1 -1
  126. package/dist/internal/funding/DepositWebhook.js +2 -1
  127. package/dist/internal/funding/DepositWebhook.js.map +1 -1
  128. package/dist/internal/funding/Evidence.d.ts.map +1 -1
  129. package/dist/internal/funding/Evidence.js +2 -10
  130. package/dist/internal/funding/Evidence.js.map +1 -1
  131. package/dist/internal/funding/Provider.d.ts +40 -0
  132. package/dist/internal/funding/Provider.d.ts.map +1 -1
  133. package/dist/internal/funding/Provider.js +5 -0
  134. package/dist/internal/funding/Provider.js.map +1 -1
  135. package/dist/internal/funding/SourceObservation.d.ts.map +1 -1
  136. package/dist/internal/funding/SourceObservation.js +5 -7
  137. package/dist/internal/funding/SourceObservation.js.map +1 -1
  138. package/dist/internal/funding/Transfer.d.ts +101 -6
  139. package/dist/internal/funding/Transfer.d.ts.map +1 -1
  140. package/dist/internal/funding/Transfer.js +162 -34
  141. package/dist/internal/funding/Transfer.js.map +1 -1
  142. package/dist/internal/funding/TransferReconciliation.d.ts +52 -0
  143. package/dist/internal/funding/TransferReconciliation.d.ts.map +1 -0
  144. package/dist/internal/funding/TransferReconciliation.js +192 -0
  145. package/dist/internal/funding/TransferReconciliation.js.map +1 -0
  146. package/dist/internal/funding/TransferWebhook.d.ts +21 -0
  147. package/dist/internal/funding/TransferWebhook.d.ts.map +1 -0
  148. package/dist/internal/funding/TransferWebhook.js +43 -0
  149. package/dist/internal/funding/TransferWebhook.js.map +1 -0
  150. package/dist/internal/funding/index.d.ts +1 -0
  151. package/dist/internal/funding/index.d.ts.map +1 -1
  152. package/dist/internal/funding/index.js +1 -0
  153. package/dist/internal/funding/index.js.map +1 -1
  154. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  155. package/dist/internal/funding/providers/relay.js +3 -12
  156. package/dist/internal/funding/providers/relay.js.map +1 -1
  157. package/dist/internal/funding/providers/stargate.d.ts +99 -0
  158. package/dist/internal/funding/providers/stargate.d.ts.map +1 -1
  159. package/dist/internal/funding/providers/stargate.js +337 -35
  160. package/dist/internal/funding/providers/stargate.js.map +1 -1
  161. package/dist/internal/webhookDestinations/slack.d.ts.map +1 -1
  162. package/dist/internal/webhookDestinations/slack.js +16 -0
  163. package/dist/internal/webhookDestinations/slack.js.map +1 -1
  164. package/package.json +1 -1
  165. package/src/App.ts +1 -0
  166. package/src/Client.test-d.ts +83 -2
  167. package/src/Webhooks.ts +2 -2
  168. package/src/admin/apps/earn-vaults.ts +2 -1
  169. package/src/admin/apps/organizations.test.ts +53 -1
  170. package/src/admin/apps/organizations.ts +90 -5
  171. package/src/admin/ui/src/lib/organizations.test.ts +112 -1
  172. package/src/admin/ui/src/lib/organizations.ts +55 -0
  173. package/src/admin/ui/src/routes/organizations_.$orgId.tsx +116 -0
  174. package/src/admin/ui.generated.ts +69 -69
  175. package/src/apps/Zones.test.ts +3 -3
  176. package/src/apps/Zones.ts +3 -3
  177. package/src/apps/composition.test.ts +1 -0
  178. package/src/apps/data/App.ts +15 -6
  179. package/src/apps/data/routes/earn.test.ts +369 -14
  180. package/src/apps/data/routes/earn.ts +548 -79
  181. package/src/apps/data/routes/exchanges.test.ts +740 -15
  182. package/src/apps/data/routes/exchanges.ts +954 -753
  183. package/src/apps/data/routes/fee-amm.ts +17 -18
  184. package/src/apps/data/routes/webhooks.test.ts +21 -5
  185. package/src/apps/data/routes/webhooks.ts +63 -8
  186. package/src/apps/funding/App.ts +12 -2
  187. package/src/apps/funding/routes/deposit-addresses.test.ts +4 -4
  188. package/src/apps/funding/routes/deposit-addresses.ts +2 -2
  189. package/src/apps/funding/routes/transfers.test.ts +510 -19
  190. package/src/apps/funding/routes/transfers.ts +271 -5
  191. package/src/apps/management/Billing.ts +18 -5
  192. package/src/apps/management/routes/webhooks.test.ts +3 -3
  193. package/src/apps/management/routes/webhooks.ts +24 -25
  194. package/src/apps/relay/App.test.ts +212 -1
  195. package/src/apps/relay/App.ts +158 -29
  196. package/src/apps/relay/Sponsorships.ts +24 -1
  197. package/src/db/Db.ts +35 -0
  198. package/src/db/Schema.ts +17 -2
  199. package/src/db/tables/fundingTransferTransactions.test.ts +43 -0
  200. package/src/db/tables/fundingTransferTransactions.ts +79 -3
  201. package/src/db/tables/fundingTransfers.test.ts +26 -0
  202. package/src/db/tables/fundingTransfers.ts +36 -0
  203. package/src/db/tables/organizations.test.ts +41 -0
  204. package/src/db/tables/organizations.ts +49 -0
  205. package/src/db/tables/projects.test.ts +55 -0
  206. package/src/db/tables/projects.ts +46 -1
  207. package/src/db/tables/sponsoredTransactions.test.ts +338 -0
  208. package/src/db/tables/sponsoredTransactions.ts +197 -13
  209. package/src/db/tables/webhookSubscriptions.ts +18 -11
  210. package/src/internal/EarnVaults.ts +5 -5
  211. package/src/internal/Log.test-d.ts +2 -0
  212. package/src/internal/Log.ts +2 -0
  213. package/src/internal/Viem.ts +39 -3
  214. package/src/internal/Webhooks.test.ts +45 -0
  215. package/src/internal/funding/DepositWebhook.ts +2 -1
  216. package/src/internal/funding/Evidence.ts +2 -13
  217. package/src/internal/funding/Provider.ts +43 -0
  218. package/src/internal/funding/SourceObservation.ts +5 -9
  219. package/src/internal/funding/Transfer.test.ts +155 -0
  220. package/src/internal/funding/Transfer.ts +254 -37
  221. package/src/internal/funding/TransferReconciliation.test.ts +147 -0
  222. package/src/internal/funding/TransferReconciliation.ts +264 -0
  223. package/src/internal/funding/TransferWebhook.test.ts +128 -0
  224. package/src/internal/funding/TransferWebhook.ts +67 -0
  225. package/src/internal/funding/index.test.ts +1 -0
  226. package/src/internal/funding/index.ts +1 -0
  227. package/src/internal/funding/providers/relay.ts +3 -15
  228. package/src/internal/funding/providers/stargate.test.ts +169 -4
  229. package/src/internal/funding/providers/stargate.ts +498 -51
  230. package/src/internal/webhookDestinations/slack.ts +15 -0
@@ -11,11 +11,13 @@ import * as Cache from '../../../internal/Cache.js'
11
11
  import * as FundingCatalog from '../../../internal/funding/Catalog.js'
12
12
  import * as FundingProvider from '../../../internal/funding/Provider.js'
13
13
  import * as FundingTransfer from '../../../internal/funding/Transfer.js'
14
+ import * as FundingTransferReconciliation from '../../../internal/funding/TransferReconciliation.js'
14
15
  import * as Idempotency from '../../../internal/Idempotency.js'
15
16
  import * as OpenApi from '../../../internal/OpenApi.js'
16
17
  import * as Response from '../../../internal/Response.js'
17
18
  import * as Schema from '../../../internal/Schema.js'
18
19
  import * as Value from '../../../internal/Value.js'
20
+ import type * as Webhooks from '../../../internal/Webhooks.js'
19
21
  import * as Access from '../Access.js'
20
22
  import * as ProviderAttempts from '../ProviderAttempts.js'
21
23
 
@@ -161,6 +163,25 @@ export namespace schema {
161
163
  /** The funding transfer resource, returned directly. */
162
164
  export const Response = Transfer
163
165
  }
166
+
167
+ /** Schemas for the registerFundingTransferSourceTransaction operation. */
168
+ export namespace registerFundingTransferSourceTransaction {
169
+ /** Source transaction registration request body. */
170
+ export const Body = OpenApi.component(
171
+ z
172
+ .strictObject({
173
+ transactionHash: Schema.hash(`0x${'51'.repeat(32)}`),
174
+ })
175
+ .check(z.describe('Verified source transaction to associate with the transfer.')),
176
+ 'RegisterFundingTransferSourceTransactionRequest',
177
+ )
178
+
179
+ /** Path parameters for source transaction registration. */
180
+ export const Params = getFundingTransfer.Params
181
+
182
+ /** The transfer after source transaction registration. */
183
+ export const Response = Transfer
184
+ }
164
185
  }
165
186
 
166
187
  const baseChainExample = {
@@ -227,6 +248,14 @@ const fundingTransferExample = {
227
248
  version: 1,
228
249
  } satisfies z.output<typeof schema.Transfer>
229
250
 
251
+ const registeredFundingTransferExample = {
252
+ ...fundingTransferExample,
253
+ sourceTransactionHashes: [`0x${'51'.repeat(32)}`],
254
+ status: 'processing',
255
+ updatedAt: '2026-01-01T00:00:20.000Z',
256
+ version: 2,
257
+ } satisfies z.output<typeof schema.registerFundingTransferSourceTransaction.Response>
258
+
230
259
  const listFundingTransfersResponseExample = {
231
260
  data: [fundingTransferExample],
232
261
  nextCursor: null,
@@ -276,9 +305,17 @@ const idempotencyReplayTtlMs = 86_400_000
276
305
  // Pending claims only cover the bounded provider preparation window.
277
306
  const idempotencyPendingTtlMs = 60_000
278
307
  const fundingEtag = etag()
308
+ const bodyValidation = {
309
+ code: 'body_invalid',
310
+ message: 'Check the request body and try again.',
311
+ } as const
312
+ const paramValidation = {
313
+ code: 'param_invalid',
314
+ message: 'Check the path parameters and try again.',
315
+ } as const
279
316
 
280
317
  /** Mounts durable funding transfer routes. */
281
- export function transfers() {
318
+ export function transfers(options: transfers.Options = {}) {
282
319
  return new Hono<App.Environment>()
283
320
  .get(
284
321
  '/v1/funding/transfers',
@@ -422,6 +459,209 @@ export function transfers() {
422
459
  }
423
460
  },
424
461
  )
462
+ .post(
463
+ '/v1/funding/transfers/:id{ftr_[A-Za-z0-9_-]+}/source-transactions',
464
+ Auth.policy({ apiKey: { scopes: ['funding:write'] } }),
465
+ OpenApi.validate(
466
+ 'param',
467
+ schema.registerFundingTransferSourceTransaction.Params,
468
+ paramValidation,
469
+ ),
470
+ OpenApi.validate(
471
+ 'json',
472
+ schema.registerFundingTransferSourceTransaction.Body,
473
+ bodyValidation,
474
+ ),
475
+ OpenApi.describeRoute({
476
+ description: 'Registers and verifies a funding source transaction.',
477
+ operationId: 'registerFundingTransferSourceTransaction',
478
+ responses: OpenApi.responses({
479
+ errors: {
480
+ 400: {
481
+ codes: ['body_invalid', 'param_invalid', 'source_transaction_invalid'],
482
+ description: 'The path, body, or submitted source transaction is invalid.',
483
+ },
484
+ 404: {
485
+ codes: ['funding_transfer_not_found'],
486
+ description: 'No visible funding transfer exists for the identifier.',
487
+ },
488
+ 409: {
489
+ codes: [
490
+ 'funding_transfer_not_awaiting_source',
491
+ 'source_transaction_conflict',
492
+ 'source_transaction_pending',
493
+ ],
494
+ description: 'The transfer or source transaction cannot be registered yet.',
495
+ },
496
+ 501: {
497
+ codes: ['not_implemented'],
498
+ description: 'Source transaction reconciliation is not configured.',
499
+ },
500
+ 502: {
501
+ codes: ['upstream_error'],
502
+ description: 'Source verification or reconciliation dispatch failed.',
503
+ },
504
+ },
505
+ success: {
506
+ description: 'The transfer with its verified source transaction.',
507
+ example: registeredFundingTransferExample,
508
+ schema: schema.registerFundingTransferSourceTransaction.Response,
509
+ },
510
+ }),
511
+ summary: 'Register source transaction',
512
+ tags: ['Funding Transfers'],
513
+ }),
514
+ async (c) => {
515
+ if (Auth.narrowAccess) return Auth.accessError(c)
516
+ if (OpenApi.narrowValidation) return OpenApi.validationError(c, paramValidation)
517
+ if (OpenApi.narrowValidation) return OpenApi.validationError(c, bodyValidation)
518
+
519
+ const owner = Access.owner(c)
520
+ if (!owner) return Access.unauthorized(c)
521
+ c.header('Cache-Control', Cache.policies.noStore)
522
+ c.header('Vary', 'Accept-Encoding, Authorization, Tempo-API-Key, X-API-Key')
523
+
524
+ const dispatch = options.dispatchTransferReconciliation
525
+ if (!dispatch)
526
+ return Response.error(c, {
527
+ code: 'not_implemented',
528
+ message: 'Source transaction reconciliation is not configured',
529
+ status: 501,
530
+ })
531
+
532
+ const db = Db.get(c.get('db'))
533
+ const { id } = c.req.valid('param')
534
+ const { transactionHash } = c.req.valid('json')
535
+ try {
536
+ const record = await FundingTransfers.getOwned(db, owner, id)
537
+ if (!record)
538
+ return Response.error(c, {
539
+ code: 'funding_transfer_not_found',
540
+ message: 'Funding transfer not found',
541
+ status: 404,
542
+ })
543
+ if (
544
+ record.snapshot.sourceTransactionHashes?.some(
545
+ (candidate) => candidate.toLowerCase() === transactionHash,
546
+ )
547
+ ) {
548
+ if (record.status === 'processing')
549
+ await dispatch({ transferId: record.id, type: 'funding:transfer:reconcile' })
550
+ return c.json(
551
+ Response.validated(
552
+ schema.registerFundingTransferSourceTransaction.Response,
553
+ FundingTransfer.toPublic(record),
554
+ ),
555
+ 200,
556
+ )
557
+ }
558
+ const provider = c
559
+ .get('providers')
560
+ .filter(FundingProvider.is)
561
+ .find((candidate) => candidate.id === record.providerId)
562
+ if (!provider || !FundingProvider.canVerifySourceTransaction(provider))
563
+ return Response.error(c, {
564
+ code: 'funding_transfer_not_awaiting_source',
565
+ message: 'Funding transfer does not accept source transactions',
566
+ status: 409,
567
+ })
568
+ if (record.status !== 'awaiting-source')
569
+ return Response.error(c, {
570
+ code: 'funding_transfer_not_awaiting_source',
571
+ message: 'Funding transfer is not awaiting a source transaction',
572
+ status: 409,
573
+ })
574
+
575
+ const catalog = await FundingCatalog.snapshot(c)
576
+ const destinationChain = catalog.chainsByKey.get(record.snapshot.destinationChain.id)
577
+ const sourceChain = catalog.chainsByKey.get(record.snapshot.sourceChain.id)
578
+ const routeConfiguration = catalog.routesByProvider
579
+ .get(record.providerId)
580
+ ?.find(
581
+ (candidate) =>
582
+ candidate.route.source.chain.id === record.snapshot.sourceChain.id &&
583
+ candidate.route.source.address.toLowerCase() ===
584
+ record.snapshot.sourceToken.address.toLowerCase() &&
585
+ candidate.route.destination.chain.id === record.snapshot.destinationChain.id &&
586
+ candidate.route.destination.address.toLowerCase() ===
587
+ record.snapshot.destinationToken.address.toLowerCase(),
588
+ )?.configuration
589
+ if (!destinationChain || !sourceChain)
590
+ return Response.error(c, {
591
+ code: 'funding_transfer_not_awaiting_source',
592
+ message: 'Funding transfer route chains are no longer configured',
593
+ status: 409,
594
+ })
595
+
596
+ const evidence = await provider.verifySourceTransaction(
597
+ {
598
+ destinationChain,
599
+ providerState: record.providerState ?? undefined,
600
+ routeConfiguration,
601
+ sourceChain,
602
+ transactionHash,
603
+ transfer: record.snapshot,
604
+ },
605
+ c.req.raw.signal,
606
+ )
607
+ if (evidence.type === 'pending')
608
+ return Response.error(c, {
609
+ code: 'source_transaction_pending',
610
+ message: 'Source transaction is not confirmed yet',
611
+ status: 409,
612
+ })
613
+ if (evidence.type === 'invalid')
614
+ return Response.error(c, {
615
+ code: 'source_transaction_invalid',
616
+ message: 'Source transaction does not match the funding transfer',
617
+ status: 400,
618
+ })
619
+
620
+ const registered = await FundingTransfer.registerSourceTransaction(db, {
621
+ chainId: record.snapshot.sourceChain.id,
622
+ id: record.id,
623
+ providerState: evidence.providerState ?? {},
624
+ transactionHash,
625
+ })
626
+ if (registered.type === 'not_found')
627
+ return Response.error(c, {
628
+ code: 'funding_transfer_not_found',
629
+ message: 'Funding transfer not found',
630
+ status: 404,
631
+ })
632
+ if (registered.type === 'conflict')
633
+ return Response.error(c, {
634
+ code: 'source_transaction_conflict',
635
+ message: 'Source transaction is already assigned to another funding transfer',
636
+ status: 409,
637
+ })
638
+ if (registered.type === 'invalid')
639
+ return Response.error(c, {
640
+ code: 'funding_transfer_not_awaiting_source',
641
+ message: 'Funding transfer is not awaiting a source transaction',
642
+ status: 409,
643
+ })
644
+
645
+ await Promise.all([
646
+ dispatch({ transferId: record.id, type: 'funding:transfer:reconcile' }),
647
+ registered.type === 'registered' &&
648
+ options.dispatchTransferUpdates &&
649
+ registered.references.length > 0
650
+ ? options.dispatchTransferUpdates(registered.references)
651
+ : undefined,
652
+ ])
653
+ return c.json(
654
+ Response.validated(
655
+ schema.registerFundingTransferSourceTransaction.Response,
656
+ FundingTransfer.toPublic(registered.record),
657
+ ),
658
+ 200,
659
+ )
660
+ } catch (cause) {
661
+ return Response.upstream(c, cause)
662
+ }
663
+ },
664
+ )
425
665
  .post(
426
666
  '/v1/funding/transfers',
427
667
  Auth.policy({ apiKey: { scopes: ['funding:write'] } }),
@@ -590,12 +830,18 @@ export function transfers() {
590
830
  // The transfer row, its version-1 event, and the replay body commit
591
831
  // atomically so a crash cannot leave a claimed key without a replay.
592
832
  const created = await db.transaction(async (tx) => {
593
- const record = await FundingTransfer.createIn(tx, {
833
+ const { record, references } = await FundingTransfer.createIn(tx, {
594
834
  apiKeyId: principal.id,
595
835
  environment: owner.environment,
596
836
  orgId: owner.orgId,
597
837
  projectId: owner.projectId,
598
- providerState: result.correlation,
838
+ providerState:
839
+ result.correlation || result.providerState
840
+ ? {
841
+ ...result.correlation,
842
+ ...result.providerState,
843
+ }
844
+ : undefined,
599
845
  snapshot,
600
846
  })
601
847
  const validated = Response.validated(
@@ -612,9 +858,15 @@ export function transfers() {
612
858
  })
613
859
  if (!completion)
614
860
  throw new IdempotencyLeaseLostError('Funding transfer creation lease was lost.')
615
- return validated
861
+ return { references, response: validated }
616
862
  })
617
- return c.json(created, 200)
863
+ if (options.dispatchTransferUpdates && created.references.length > 0)
864
+ try {
865
+ await options.dispatchTransferUpdates(created.references)
866
+ } catch (cause) {
867
+ return Response.upstream(c, cause)
868
+ }
869
+ return c.json(created.response, 200)
618
870
  } catch (cause) {
619
871
  await release()
620
872
  if (cause instanceof FundingProvider.InvalidAmountError)
@@ -708,6 +960,20 @@ export function transfers() {
708
960
  )
709
961
  }
710
962
 
963
+ export declare namespace transfers {
964
+ /** Funding transfer route dependencies. */
965
+ type Options = {
966
+ /** Enqueues one registered transfer for destination reconciliation. */
967
+ dispatchTransferReconciliation?:
968
+ | ((message: FundingTransferReconciliation.Message) => Promise<void>)
969
+ | undefined
970
+ /** Enqueues webhook updates staged with transfer mutations. */
971
+ dispatchTransferUpdates?:
972
+ | ((references: readonly Webhooks.QueueReference[]) => Promise<number>)
973
+ | undefined
974
+ }
975
+ }
976
+
711
977
  function quoteNotAvailable(c: Context<App.Environment>) {
712
978
  return Response.error(c, {
713
979
  code: 'quote_not_available',
@@ -1,7 +1,7 @@
1
1
  import { Hash, Hex, Value } from 'ox'
2
2
  import type Stripe from 'stripe'
3
3
 
4
- import type * as Store from '../../internal/Store.js'
4
+ import * as ApiKey from '../../ApiKey.js'
5
5
  import * as ApiKeys from '../../ApiKeys.js'
6
6
  import type * as BillingSettings from '../../db/tables/billingSettings.js'
7
7
  import * as Db from '../../db/Db.js'
@@ -9,6 +9,7 @@ import * as Fees from '../../internal/Fees.js'
9
9
  import * as RequestUsage from '../../db/tables/requestUsage.js'
10
10
  import type * as Scope from '../../Scope.js'
11
11
  import * as SponsoredTransactions from '../../db/tables/sponsoredTransactions.js'
12
+ import type * as Store from '../../internal/Store.js'
12
13
  import * as StripeCustomers from '../../db/tables/stripeCustomers.js'
13
14
  import * as Viem from '../../internal/Viem.js'
14
15
 
@@ -567,10 +568,22 @@ export function createRequestUsageReporter(
567
568
  reported++
568
569
  } catch (error) {
569
570
  failed++
570
- console.error(
571
- `Billing.createRequestUsageReporter: reporting ${environment} bucket ${orgId}/${bucketStart} failed`,
572
- error,
573
- )
571
+ // Cloudflare flattens multiple console arguments, so keep the driver details on one structured entry.
572
+ const cause = error instanceof Error ? error : undefined
573
+ console.error({
574
+ bucketStart,
575
+ component: 'request_usage_meter',
576
+ environment,
577
+ error: ApiKey.redact(cause?.message ?? String(error)).slice(0, 500),
578
+ ...(cause &&
579
+ 'code' in cause &&
580
+ (typeof cause.code === 'string' || typeof cause.code === 'number')
581
+ ? { errorCode: ApiKey.redact(String(cause.code)).slice(0, 100) }
582
+ : {}),
583
+ errorType: cause?.name ?? typeof error,
584
+ orgId,
585
+ ...(cause?.stack ? { stack: ApiKey.redact(cause.stack).slice(0, 4_000) } : {}),
586
+ })
574
587
  }
575
588
  }
576
589
  }
@@ -11,7 +11,7 @@ import * as Webhooks from './webhooks.js'
11
11
 
12
12
  const origin = 'http://localhost'
13
13
  const webhook = {
14
- applicationEventTypes: ['funding:deposit.updated'],
14
+ applicationEventTypes: ['funding:deposit.updated', 'funding:transfer.updated'],
15
15
  supportedChainIds: [4217, 42431],
16
16
  } satisfies App.Webhook
17
17
 
@@ -284,7 +284,7 @@ describe('webhooks', () => {
284
284
  `)
285
285
  })
286
286
 
287
- test('creates funding deposit subscriptions without reading the RPC head', async () => {
287
+ test('creates funding transfer subscriptions without reading the RPC head', async () => {
288
288
  const db = TestApp.database()
289
289
  const app = createApp({ db })
290
290
  const cookie = await session(app)
@@ -295,7 +295,7 @@ describe('webhooks', () => {
295
295
  chainId: 4217,
296
296
  destination: { type: 'url', url: 'https://example.com/funding' },
297
297
  environment: 'production',
298
- eventType: 'funding:deposit.updated',
298
+ eventType: 'funding:transfer.updated',
299
299
  filters: { status: 'completed' },
300
300
  }),
301
301
  )
@@ -8,7 +8,7 @@ import * as OpenApi from '../../../internal/OpenApi.js'
8
8
  import * as Response from '../../../internal/Response.js'
9
9
  import * as WebhookDestination from '../../../internal/WebhookDestination.js'
10
10
  import * as Webhooks from '../../../internal/Webhooks.js'
11
- import { schema as webhookSchema } from '../../data/routes/webhooks.js'
11
+ import * as DataWebhooks from '../../data/routes/webhooks.js'
12
12
  import type { Environment } from '../App.js'
13
13
 
14
14
  const defaultMaxPerOwner = 100
@@ -21,11 +21,11 @@ export namespace schema {
21
21
  export const Subscription = OpenApi.component(
22
22
  z
23
23
  .object({
24
- ...webhookSchema.Subscription.shape,
25
- context: webhookSchema.Subscription.shape.context.check(
24
+ ...DataWebhooks.schema.Subscription.shape,
25
+ context: DataWebhooks.schema.Subscription.shape.context.check(
26
26
  z.meta({ examples: [{ title: 'Production transfers' }] }),
27
27
  ),
28
- destination: webhookSchema.Subscription.shape.destination.check(
28
+ destination: DataWebhooks.schema.Subscription.shape.destination.check(
29
29
  z.meta({ examples: [{ type: 'url', url: 'https://example.com/webhooks' }] }),
30
30
  ),
31
31
  })
@@ -35,7 +35,7 @@ export namespace schema {
35
35
 
36
36
  /** Webhook delivery data returned in collection rows. */
37
37
  export const Delivery = OpenApi.component(
38
- webhookSchema.Delivery,
38
+ DataWebhooks.schema.Delivery,
39
39
  'OrganizationWebhookDeliverySummary',
40
40
  )
41
41
 
@@ -44,7 +44,7 @@ export namespace schema {
44
44
  const Environment = z
45
45
  .enum(['production', 'sandbox'])
46
46
  .check(
47
- z.describe('API-key environment whose private funding deposits are delivered.'),
47
+ z.describe('API-key environment whose private funding resources are delivered.'),
48
48
  z.meta({ examples: ['production'] }),
49
49
  )
50
50
 
@@ -52,24 +52,24 @@ export namespace schema {
52
52
  export const Body = OpenApi.component(
53
53
  z
54
54
  .intersection(
55
- webhookSchema.createWebhook.Body,
55
+ DataWebhooks.schema.createWebhook.Body,
56
56
  z.object({
57
- chainId: webhookSchema.Subscription.shape.chainId,
57
+ chainId: DataWebhooks.schema.Subscription.shape.chainId,
58
58
  environment: z.optional(Environment),
59
59
  }),
60
60
  )
61
61
  .check(
62
62
  z.refine(
63
63
  (body) =>
64
- body.eventType !== 'funding:deposit.updated' || body.environment !== undefined,
65
- { error: '`environment` is required for funding deposit subscriptions.' },
64
+ !DataWebhooks.isFundingEventType(body.eventType) || body.environment !== undefined,
65
+ { error: '`environment` is required for funding subscriptions.' },
66
66
  ),
67
67
  z.describe('Details for creating an organization webhook subscription.'),
68
68
  ),
69
69
  'CreateOrganizationWebhookRequest',
70
70
  )
71
71
 
72
- const secret = webhookSchema.createWebhook.Response.shape.secret
72
+ const secret = DataWebhooks.schema.createWebhook.Response.shape.secret
73
73
  const urlDestination = z
74
74
  .object({
75
75
  type: z
@@ -192,7 +192,7 @@ export namespace schema {
192
192
  /** Schemas for listing an organization webhook's delivery attempts. */
193
193
  export namespace listWebhookDeliveries {
194
194
  export const Params = WebhookParams
195
- export const Query = webhookSchema.listWebhookDeliveries.Query
195
+ export const Query = DataWebhooks.schema.listWebhookDeliveries.Query
196
196
  export const Response = OpenApi.component(
197
197
  z
198
198
  .object({
@@ -202,10 +202,10 @@ export namespace schema {
202
202
  z.describe('Webhook delivery attempts on this page.'),
203
203
  z.meta({ examples: [[]] }),
204
204
  ),
205
- meta: webhookSchema.listWebhookDeliveries.Response.shape.meta.check(
205
+ meta: DataWebhooks.schema.listWebhookDeliveries.Response.shape.meta.check(
206
206
  z.meta({ examples: [{ totalCount: 1, totalCountCapped: false }] }),
207
207
  ),
208
- nextCursor: webhookSchema.listWebhookDeliveries.Response.shape.nextCursor,
208
+ nextCursor: DataWebhooks.schema.listWebhookDeliveries.Response.shape.nextCursor,
209
209
  })
210
210
  .check(z.describe("A page of an organization's webhook delivery attempts.")),
211
211
  'OrganizationWebhookDeliveryList',
@@ -214,7 +214,7 @@ export namespace schema {
214
214
 
215
215
  const DeliveryParams = z
216
216
  .object({
217
- deliveryId: webhookSchema.retryWebhookDelivery.Params.shape.deliveryId,
217
+ deliveryId: DataWebhooks.schema.retryWebhookDelivery.Params.shape.deliveryId,
218
218
  id: WebhookParams.shape.id,
219
219
  orgId: WebhookParams.shape.orgId,
220
220
  })
@@ -227,7 +227,7 @@ export namespace schema {
227
227
  z
228
228
  .object({
229
229
  ...Delivery.shape,
230
- envelope: webhookSchema.Envelope,
230
+ envelope: DataWebhooks.schema.Envelope,
231
231
  })
232
232
  .check(z.describe('A webhook delivery attempt with the exact JSON payload Tempo sent.')),
233
233
  'OrganizationWebhookDelivery',
@@ -238,7 +238,7 @@ export namespace schema {
238
238
  export namespace retryWebhookDelivery {
239
239
  export const Params = DeliveryParams
240
240
  export const Response = OpenApi.component(
241
- webhookSchema.retryWebhookDelivery.Response,
241
+ DataWebhooks.schema.retryWebhookDelivery.Response,
242
242
  'RetryOrganizationWebhookDeliveryResponse',
243
243
  )
244
244
  }
@@ -261,7 +261,7 @@ export namespace schema {
261
261
  /** Lifecycle state editable from the console. */
262
262
  export const UpdateBody = OpenApi.component(
263
263
  z
264
- .object({ status: webhookSchema.Status })
264
+ .object({ status: DataWebhooks.schema.Status })
265
265
  .check(z.describe('New delivery state for a webhook subscription.')),
266
266
  'UpdateOrganizationWebhookRequest',
267
267
  )
@@ -344,7 +344,7 @@ export function webhooks(options: webhooks.Options = {}) {
344
344
 
345
345
  const body = c.req.valid('json')
346
346
  if (
347
- body.eventType === 'funding:deposit.updated' &&
347
+ DataWebhooks.isFundingEventType(body.eventType) &&
348
348
  !options.webhook.applicationEventTypes?.includes(body.eventType)
349
349
  )
350
350
  return Response.error(c, {
@@ -364,17 +364,16 @@ export function webhooks(options: webhooks.Options = {}) {
364
364
  const owner_ = owner(Auth.org(c).id)
365
365
  if ((await Webhooks.countSubscriptions(db, owner_)) >= maxPerOwner)
366
366
  throw new Webhooks.LimitExceededError(maxPerOwner)
367
- const startBlockNumber =
368
- body.eventType === 'funding:deposit.updated'
369
- ? undefined
370
- : Number(await c.get('getClient')(chainId).getBlockNumber())
367
+ const startBlockNumber = DataWebhooks.isFundingEventType(body.eventType)
368
+ ? undefined
369
+ : Number(await c.get('getClient')(chainId).getBlockNumber())
371
370
  const subscription = await Webhooks.createSubscription(
372
371
  db,
373
372
  {
374
373
  chainId,
375
374
  ...(body.context === undefined ? {} : { context: body.context }),
376
375
  destination: body.destination,
377
- ...(body.eventType === 'funding:deposit.updated' && body.environment !== undefined
376
+ ...(DataWebhooks.isFundingEventType(body.eventType) && body.environment !== undefined
378
377
  ? { environment: body.environment }
379
378
  : {}),
380
379
  eventType: body.eventType,
@@ -754,7 +753,7 @@ function publicSubscription(subscription: Webhooks.Subscription) {
754
753
  ...(subscription.context === undefined ? {} : { context: subscription.context }),
755
754
  createdAt: subscription.createdAt,
756
755
  destination,
757
- ...(subscription.eventType === 'funding:deposit.updated' &&
756
+ ...(DataWebhooks.isFundingEventType(subscription.eventType) &&
758
757
  subscription.environment !== undefined
759
758
  ? { environment: subscription.environment }
760
759
  : {}),