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
@@ -310,11 +310,11 @@ function privateResourceAccess(access: Webhooks.Access) {
310
310
  )
311
311
  return (eb: ExpressionBuilder<Db.Database, 'webhook_subscriptions'>) =>
312
312
  eb.or([
313
- eb('eventType', '!=', 'funding:deposit.updated'),
313
+ sql<boolean>`${eb.ref('eventType')} not like ${'funding:%'}`,
314
314
  ...(fundingRead
315
315
  ? [
316
316
  eb.and([
317
- eb('eventType', '=', 'funding:deposit.updated'),
317
+ sql<boolean>`${eb.ref('eventType')} like ${'funding:%'}`,
318
318
  eb('environment', '=', access.environment),
319
319
  ...(access.projectId === undefined ? [] : [eb('projectId', '=', access.projectId)]),
320
320
  ]),
@@ -390,41 +390,48 @@ export declare namespace listActiveForChain {
390
390
  }
391
391
  }
392
392
 
393
- /** Lists live funding deposit subscriptions visible to one deposit owner. */
394
- export async function listActiveForFundingDeposit(
393
+ /** Lists live funding subscriptions visible to one resource owner. */
394
+ export async function listActiveForFundingEvent(
395
395
  db: Db.Db,
396
- options: listActiveForFundingDeposit.Options,
396
+ options: listActiveForFundingEvent.Options,
397
397
  ): Promise<Webhooks.Subscription[]> {
398
398
  let query = db.kysely
399
399
  .selectFrom('webhook_subscriptions')
400
400
  .selectAll()
401
401
  .where('chainId', '=', options.chainId)
402
402
  .where('environment', '=', options.environment)
403
- .where('eventType', '=', 'funding:deposit.updated')
403
+ .where('eventType', '=', options.eventType)
404
404
  .where('ownerId', '=', options.orgId)
405
405
  .where('ownerType', '=', 'api_key')
406
406
  .where('status', '=', 'active')
407
407
  .where(live(options.now))
408
+ if (options.eventCreatedAt !== undefined)
409
+ query = query.where('createdAt', '<=', options.eventCreatedAt)
408
410
  const projectId = options.projectId
409
411
  query =
410
412
  projectId === undefined
411
413
  ? query.where('projectId', 'is', null)
412
414
  : query.where((eb) => eb.or([eb('projectId', 'is', null), eb('projectId', '=', projectId)]))
413
- return (await query.orderBy('id').execute()).map(toSubscription)
415
+ // Keep subscriptions present until their staged queue events commit.
416
+ return (await query.orderBy('id').forKeyShare().execute()).map(toSubscription)
414
417
  }
415
418
 
416
- export declare namespace listActiveForFundingDeposit {
417
- /** Ownership scope and chain for a funding deposit event. */
419
+ export declare namespace listActiveForFundingEvent {
420
+ /** Ownership scope and chain for a funding resource event. */
418
421
  type Options = {
419
422
  /** Destination Tempo chain id. */
420
423
  chainId: number
421
- /** Deposit API-key environment. */
424
+ /** Resource API-key environment. */
422
425
  environment: 'production' | 'sandbox'
426
+ /** Event timestamp used to keep subscriptions future-only, when supplied. */
427
+ eventCreatedAt?: string | undefined
428
+ /** Funding event type to match. */
429
+ eventType: Extract<Webhooks.EventType, `funding:${string}`>
423
430
  /** ISO timestamp used to exclude expired subscriptions. */
424
431
  now: string
425
432
  /** Owning organization id. */
426
433
  orgId: string
427
- /** Attributed deposit project, when present. */
434
+ /** Attributed funding resource project, when present. */
428
435
  projectId?: string | undefined
429
436
  }
430
437
  }
@@ -157,7 +157,7 @@ export namespace schema {
157
157
  ),
158
158
  vaultAddress: Schema.Address.check(
159
159
  z.describe('Earn vault contract address.'),
160
- z.meta({ examples: ['0xf4ae63687d6753a78e7f551d2eda1d0d31a5ea3a'] }),
160
+ z.meta({ examples: ['0x4f94590b636f5878bce585e82379de81e1ec174f'] }),
161
161
  ),
162
162
  })
163
163
  .check(z.describe('Onchain identity of an Earn vault.'))
@@ -167,7 +167,7 @@ export namespace schema {
167
167
  .strictObject({
168
168
  chainId: Schema.ChainId.check(
169
169
  z.describe('Private Zone chain id.'),
170
- z.meta({ examples: [1424310003] }),
170
+ z.meta({ examples: [421700001] }),
171
171
  ),
172
172
  deploymentBlock: z
173
173
  .number()
@@ -175,11 +175,11 @@ export namespace schema {
175
175
  z.int(),
176
176
  z.nonnegative(),
177
177
  z.describe('Parent-chain block that deployed the Earn router.'),
178
- z.meta({ examples: [31730087] }),
178
+ z.meta({ examples: [35816964] }),
179
179
  ),
180
180
  earnRouter: Schema.Address.check(
181
181
  z.describe('Parent-chain router for this Zone and Earn vault.'),
182
- z.meta({ examples: ['0xebedc74ffeead186acc6cac05eb82fb9e093a7c7'] }),
182
+ z.meta({ examples: ['0x8117e0ba6239b9695f780deb010f72a2fa4bdfb6'] }),
183
183
  ),
184
184
  })
185
185
  .check(z.describe('Curated Earn route through one private Zone.'))
@@ -239,7 +239,7 @@ export namespace schema {
239
239
  state,
240
240
  vaultAddress: Schema.Address.check(
241
241
  z.describe('Earn vault contract address.'),
242
- z.meta({ examples: ['0xf4ae63687d6753a78e7f551d2eda1d0d31a5ea3a'] }),
242
+ z.meta({ examples: ['0x4f94590b636f5878bce585e82379de81e1ec174f'] }),
243
243
  ),
244
244
  })
245
245
  .check(z.describe('Live state resolved from an Earn vault.'))
@@ -29,6 +29,8 @@ test('bounds sponsorship diagnostics', () => {
29
29
  'fee_token_unsupported',
30
30
  'internal_error',
31
31
  'production_api_key_required',
32
+ 'project_id_invalid',
33
+ 'project_id_required',
32
34
  'spend_limit_exceeded',
33
35
  'tx_fee_limit_exceeded',
34
36
  ] as const satisfies readonly Log.SponsorshipReason[]
@@ -104,6 +104,8 @@ export type SponsorshipReason =
104
104
  | 'fee_token_unsupported'
105
105
  | 'internal_error'
106
106
  | 'production_api_key_required'
107
+ | 'project_id_invalid'
108
+ | 'project_id_required'
107
109
  | 'spend_limit_exceeded'
108
110
  | 'tx_fee_limit_exceeded'
109
111
 
@@ -1,6 +1,12 @@
1
1
  import { ZoneRpcAuthentication } from 'ox/tempo'
2
- import { createClient, http, publicActions } from 'viem'
3
- import type { Chain as core_Chain, Client as core_Client, HttpTransport, PublicActions } from 'viem'
2
+ import { createClient as core_createClient, fallback, http, publicActions } from 'viem'
3
+ import type {
4
+ Chain as core_Chain,
5
+ Client as core_Client,
6
+ FallbackTransport,
7
+ HttpTransport,
8
+ PublicActions,
9
+ } from 'viem'
4
10
  import { tempoActions } from 'viem/tempo'
5
11
  import type { TempoActions } from 'viem/tempo'
6
12
  import { tempoMainnet, tempoTestnet } from 'viem/tempo/chains'
@@ -51,6 +57,36 @@ export type UrlResolver =
51
57
  /** Headers required when connecting to a Zone upstream. */
52
58
  export type ZoneHeaders = Record<string, string>
53
59
 
60
+ /** Creates a generic EVM client with ordered RPC endpoint fallback. */
61
+ export function createEvmClient(options: createEvmClient.Options): createEvmClient.ReturnType {
62
+ return core_createClient({
63
+ transport: fallback(
64
+ options.urls.map((url) =>
65
+ http(url, {
66
+ fetchFn: options.fetch,
67
+ ...(options.signal ? { fetchOptions: { signal: options.signal } } : {}),
68
+ retryCount: 0,
69
+ }),
70
+ ),
71
+ ),
72
+ })
73
+ }
74
+
75
+ export declare namespace createEvmClient {
76
+ /** Generic EVM RPC client configuration. */
77
+ type Options = {
78
+ /** Fetch implementation used for RPC requests. */
79
+ fetch: typeof globalThis.fetch
80
+ /** Optional request cancellation signal. */
81
+ signal?: AbortSignal | undefined
82
+ /** Ordered RPC endpoint URLs. */
83
+ urls: readonly string[]
84
+ }
85
+
86
+ /** Generic EVM client with fallback transport. */
87
+ type ReturnType = core_Client<FallbackTransport, undefined, undefined>
88
+ }
89
+
54
90
  /** Returns chain ids declared by a static chain-id-to-URL map. */
55
91
  export function configuredChainIds(value: UrlResolver | undefined): number[] {
56
92
  const urls = parseUrls(value)
@@ -158,7 +194,7 @@ export function getClient(options: getClient.Options): getClient.ReturnType {
158
194
  }
159
195
  : undefined
160
196
 
161
- return createClient({
197
+ return core_createClient({
162
198
  // Auto-batch `eth_call`s within the same macrotask into a single
163
199
  // deployless multicall. Lets call sites issue `Promise.all` of N reads
164
200
  // (e.g. `balanceOf` across many tokens) and pay one RPC round-trip.
@@ -1,5 +1,7 @@
1
1
  import { Webhooks } from 'tapimo'
2
+ import { sql } from 'kysely'
2
3
  import * as TestApp from '../../test/App.js'
4
+ import * as WebhookSubscriptions from '../db/tables/webhookSubscriptions.js'
3
5
  import * as Metrics from '../Metrics.js'
4
6
  import * as Cursor from './Cursor.js'
5
7
 
@@ -170,6 +172,49 @@ describe('createSubscription', () => {
170
172
  })
171
173
  })
172
174
 
175
+ describe('listActiveForFundingEvent', () => {
176
+ test('keeps selected subscriptions until the caller transaction commits', async () => {
177
+ const getDb = TestApp.databaseFactory()
178
+ const owner = { orgId: 'org_funding_subscription_lock', type: 'api_key' } as const
179
+ const subscription = await Webhooks.createSubscription(
180
+ getDb(),
181
+ createInput({
182
+ environment: 'production',
183
+ eventType: 'funding:transfer.updated',
184
+ owner,
185
+ }),
186
+ )
187
+ const locked = Promise.withResolvers<void>()
188
+ const release = Promise.withResolvers<void>()
189
+ const read = getDb().transaction(async (tx) => {
190
+ const subscriptions = await WebhookSubscriptions.listActiveForFundingEvent(tx, {
191
+ chainId: 4217,
192
+ environment: 'production',
193
+ eventType: 'funding:transfer.updated',
194
+ now: new Date().toISOString(),
195
+ orgId: owner.orgId,
196
+ })
197
+ locked.resolve()
198
+ await release.promise
199
+ return subscriptions
200
+ })
201
+ await locked.promise
202
+
203
+ const deletion = await getDb()
204
+ .transaction(async (tx) => {
205
+ await sql`select set_config('lock_timeout', '100ms', true)`.execute(tx.kysely)
206
+ return Webhooks.deleteSubscription(tx, owner, subscription.id)
207
+ })
208
+ .catch((cause: unknown) => cause)
209
+ release.resolve()
210
+
211
+ await expect(read).resolves.toEqual([subscription])
212
+ expect(deletion).toBeInstanceOf(Error)
213
+ expect(String(deletion)).toContain('lock timeout')
214
+ await expect(Webhooks.deleteSubscription(getDb(), owner, subscription.id)).resolves.toBe(true)
215
+ })
216
+ })
217
+
173
218
  describe('owner isolation', () => {
174
219
  test('an owner cannot read or list another owner subscriptions', async () => {
175
220
  const db = TestApp.database()
@@ -37,9 +37,10 @@ export async function stage(
37
37
  for (const record of unique(options.records)) {
38
38
  const chainId = destinationChainId(record)
39
39
  if (chainId === undefined) continue
40
- const subscriptions = await WebhookSubscriptions.listActiveForFundingDeposit(db, {
40
+ const subscriptions = await WebhookSubscriptions.listActiveForFundingEvent(db, {
41
41
  chainId,
42
42
  environment: record.environment,
43
+ eventType: 'funding:deposit.updated',
43
44
  now,
44
45
  orgId: record.orgId,
45
46
  ...(record.projectId === null ? {} : { projectId: record.projectId }),
@@ -1,5 +1,5 @@
1
1
  import { AbiEvent, Address, Base58, Bytes, Hash, Hex, RpcSchema, RpcTransport } from 'ox'
2
- import { createClient, custom, fallback, TransactionReceiptNotFoundError } from 'viem'
2
+ import { TransactionReceiptNotFoundError } from 'viem'
3
3
  import { getBlockNumber, getTransactionReceipt } from 'viem/actions'
4
4
  import * as z from 'zod/mini'
5
5
 
@@ -195,7 +195,7 @@ async function verifyEvm(
195
195
  parameters: Reconciliation.VerifyTransfers.Parameters,
196
196
  options: FetchOptions & { confirmations: number },
197
197
  ): Promise<readonly Reconciliation.TransferEvidence[]> {
198
- const client = jsonRpcClient(options)
198
+ const client = Viem.createEvmClient(options)
199
199
  const [receipt, head] = await Promise.all([
200
200
  getTransactionReceipt(client, { hash: parameters.transactionHash as Hex.Hex }).catch(
201
201
  (cause) => {
@@ -334,17 +334,6 @@ async function verifyTron(
334
334
  })
335
335
  }
336
336
 
337
- function jsonRpcClient(options: FetchOptions) {
338
- return createClient({
339
- transport: fallback(
340
- options.urls.map((url) => {
341
- const transport = RpcTransport.fromHttp(url, { fetchFn: options.fetch })
342
- return custom(transport, { retryCount: 0 })
343
- }),
344
- ),
345
- })
346
- }
347
-
348
337
  async function requestSolana(
349
338
  options: FetchOptions,
350
339
  request: SolanaRpcSchema['Request'],
@@ -232,6 +232,7 @@ export namespace schema {
232
232
  token: z.optional(TokenRef),
233
233
  }),
234
234
  ),
235
+ providerState: z.optional(z.record(z.string(), z.unknown())),
235
236
  quality: GetQuoteResult.shape.quality,
236
237
  sampledAt: z.iso.datetime(),
237
238
  })
@@ -485,6 +486,36 @@ export declare namespace validateTransferAction {
485
486
  }
486
487
  }
487
488
 
489
+ /** Contract for provider-specific source transaction verification. */
490
+ export declare namespace verifySourceTransaction {
491
+ /** Stored transfer terms and submitted source transaction. */
492
+ type Parameters = {
493
+ /** Configured chain that receives the provider delivery. */
494
+ destinationChain: Chain
495
+ /** Private provider state captured while preparing the transfer. */
496
+ providerState?: Transfer.ProviderState | undefined
497
+ /** Current configuration for the stored provider route. */
498
+ routeConfiguration?: core_Catalog.Configuration | undefined
499
+ /** Configured chain containing the submitted transaction. */
500
+ sourceChain: Chain
501
+ /** Submitted transaction reference. */
502
+ transactionHash: string
503
+ /** Stored public transfer terms. */
504
+ transfer: Transfer.Snapshot
505
+ }
506
+
507
+ /** Source transaction verification outcome. */
508
+ type ReturnType =
509
+ | { type: 'invalid' }
510
+ | { type: 'pending' }
511
+ | {
512
+ /** Private provider evidence retained for reconciliation. */
513
+ providerState?: Transfer.ProviderState | undefined
514
+ /** The transaction matches the stored transfer terms. */
515
+ type: 'verified'
516
+ }
517
+ }
518
+
488
519
  /** Contract of the provider `createDepositAddress` method. */
489
520
  export declare namespace createDepositAddress {
490
521
  /** Input passed to a provider provisioning one reusable deposit address. */
@@ -705,6 +736,11 @@ export type Provider<id extends string = string> = core_Provider.Provider<id, 'f
705
736
  validateTransferAction?(
706
737
  input: validateTransferAction.Parameters,
707
738
  ): validateTransferAction.ReturnType
739
+ /** Verifies a submitted source transaction, when supported. */
740
+ verifySourceTransaction?(
741
+ input: verifySourceTransaction.Parameters,
742
+ signal: AbortSignal,
743
+ ): Promise<verifySourceTransaction.ReturnType>
708
744
  /** Receives authenticated provider status events, when configured. */
709
745
  webhook?: Webhook | undefined
710
746
  }
@@ -756,6 +792,13 @@ export function canObserveDepositAddressRequest(provider: Provider): provider is
756
792
  return typeof provider.observeDepositAddressRequest === 'function'
757
793
  }
758
794
 
795
+ /** Narrows a funding provider to one that verifies submitted source transactions. */
796
+ export function canVerifySourceTransaction(provider: Provider): provider is Provider & {
797
+ verifySourceTransaction: NonNullable<Provider['verifySourceTransaction']>
798
+ } {
799
+ return typeof provider.verifySourceTransaction === 'function'
800
+ }
801
+
759
802
  /** Narrows a funding provider to one with authenticated webhook receipt. */
760
803
  export function canReceiveWebhook(provider: Provider): provider is Provider & {
761
804
  webhook: Webhook
@@ -1,10 +1,10 @@
1
- import { AbiEvent, Address, RpcTransport } from 'ox'
2
- import { createClient, custom, fallback } from 'viem'
1
+ import { AbiEvent, Address } from 'ox'
3
2
  import { getBlockNumber, getLogs } from 'viem/actions'
4
3
 
5
4
  import type * as Db from '../../db/Db.js'
6
5
  import * as FundingDepositAddresses from '../../db/tables/fundingDepositAddresses.js'
7
6
  import * as FundingDeposits from '../../db/tables/fundingDeposits.js'
7
+ import * as Viem from '../Viem.js'
8
8
  import * as Value from '../Value.js'
9
9
  import * as Deposit from './Deposit.js'
10
10
 
@@ -52,13 +52,9 @@ export async function scan(options: scan.Options): Promise<scan.Result> {
52
52
  throw new ConfigurationError('source token address')
53
53
  if (options.rpcUrls.length === 0) throw new ConfigurationError('source RPC URL')
54
54
 
55
- const client = createClient({
56
- transport: fallback(
57
- options.rpcUrls.map((url) => {
58
- const transport = RpcTransport.fromHttp(url, { fetchFn: options.fetch ?? globalThis.fetch })
59
- return custom(transport, { retryCount: 0 })
60
- }),
61
- ),
55
+ const client = Viem.createEvmClient({
56
+ fetch: options.fetch ?? globalThis.fetch,
57
+ urls: options.rpcUrls,
62
58
  })
63
59
  const head = await getBlockNumber(client, { cacheTime: 0 })
64
60
  const confirmations = BigInt(options.confirmations ?? defaultConfirmations)
@@ -42,6 +42,7 @@ describe('canTransition', () => {
42
42
  "completed": [],
43
43
  "expired": [],
44
44
  "processing": [
45
+ "awaiting-source",
45
46
  "refunding",
46
47
  "completed",
47
48
  "action-required",
@@ -129,6 +130,160 @@ describe('create', () => {
129
130
  })
130
131
  })
131
132
 
133
+ describe('registerSourceTransaction', () => {
134
+ const transactionHash = `0x${'a2'.repeat(32)}`
135
+
136
+ test('atomically assigns source evidence and starts reconciliation', async () => {
137
+ const record = await Transfer.create(
138
+ db,
139
+ createInput({
140
+ providerState: {
141
+ routeConfiguration: {
142
+ destinationPoolAddress: `0x${'cc'.repeat(20)}`,
143
+ poolAddress: `0x${'dd'.repeat(20)}`,
144
+ sourceEid: 30_184,
145
+ },
146
+ },
147
+ snapshot: TestFunding.transferSnapshot({ provider: { id: 'stargate', name: 'Stargate' } }),
148
+ }),
149
+ )
150
+ vi.setSystemTime(new Date('2026-01-01T00:00:10.000Z'))
151
+ const result = await Transfer.registerSourceTransaction(db, {
152
+ chainId: 'eip155:8453',
153
+ id: record.id,
154
+ providerState: { stargate: { guid: `0x${'bb'.repeat(32)}`, sourceEid: 30_184 } },
155
+ transactionHash: `0x${'a1'.repeat(32)}`,
156
+ })
157
+ expect(result.type).toBe('registered')
158
+ if (result.type !== 'registered') return
159
+ expect({
160
+ providerState: result.record.providerState,
161
+ sourceTransactionHashes: result.record.snapshot.sourceTransactionHashes,
162
+ status: result.record.status,
163
+ version: result.record.version,
164
+ }).toMatchInlineSnapshot(`
165
+ {
166
+ "providerState": {
167
+ "routeConfiguration": {
168
+ "destinationPoolAddress": "0xcccccccccccccccccccccccccccccccccccccccc",
169
+ "poolAddress": "0xdddddddddddddddddddddddddddddddddddddddd",
170
+ "sourceEid": 30184,
171
+ },
172
+ "stargate": {
173
+ "guid": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
174
+ "sourceEid": 30184,
175
+ },
176
+ },
177
+ "sourceTransactionHashes": [
178
+ "0xa1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
179
+ ],
180
+ "status": "processing",
181
+ "version": 2,
182
+ }
183
+ `)
184
+ })
185
+
186
+ test('replays the same assignment and rejects reuse by another transfer', async () => {
187
+ const first = await Transfer.create(db, createInput())
188
+ const second = await Transfer.create(db, createInput())
189
+ const options = {
190
+ chainId: 'eip155:8453',
191
+ id: first.id,
192
+ providerState: { stargate: { guid: `0x${'bb'.repeat(32)}`, sourceEid: 30_184 } },
193
+ transactionHash,
194
+ }
195
+ await Transfer.registerSourceTransaction(db, options)
196
+ const replayed = await Transfer.registerSourceTransaction(db, options)
197
+ const conflict = await Transfer.registerSourceTransaction(db, { ...options, id: second.id })
198
+ expect({
199
+ conflict: conflict.type,
200
+ replayed: replayed.type,
201
+ replayedVersion: replayed.type === 'replayed' ? replayed.record.version : undefined,
202
+ }).toEqual({ conflict: 'conflict', replayed: 'replayed', replayedVersion: 2 })
203
+ })
204
+ })
205
+
206
+ describe('reopenSourceTransaction', () => {
207
+ test('removes invalidated source evidence and accepts a replacement', async () => {
208
+ const record = await Transfer.create(db, createInput())
209
+ const transactionHash = `0x${'a4'.repeat(32)}`
210
+ await Transfer.registerSourceTransaction(db, {
211
+ chainId: 'eip155:8453',
212
+ id: record.id,
213
+ providerState: { stargate: { guid: `0x${'bb'.repeat(32)}`, sourceEid: 30_184 } },
214
+ transactionHash,
215
+ })
216
+
217
+ const reopened = await Transfer.reopenSourceTransaction(db, {
218
+ chainId: 'eip155:8453',
219
+ id: record.id,
220
+ providerState: { routeConfiguration: { poolAddress: `0x${'cc'.repeat(20)}` } },
221
+ transactionHash: transactionHash.toUpperCase(),
222
+ })
223
+ expect(reopened.type).toBe('reopened')
224
+ if (reopened.type !== 'reopened') return
225
+ expect({
226
+ providerState: reopened.record.providerState,
227
+ sourceTransactionHashes: reopened.record.snapshot.sourceTransactionHashes,
228
+ status: reopened.record.status,
229
+ version: reopened.record.version,
230
+ }).toEqual({
231
+ providerState: { routeConfiguration: { poolAddress: `0x${'cc'.repeat(20)}` } },
232
+ sourceTransactionHashes: undefined,
233
+ status: 'awaiting-source',
234
+ version: 3,
235
+ })
236
+
237
+ const replacement = await Transfer.registerSourceTransaction(db, {
238
+ chainId: 'eip155:8453',
239
+ id: record.id,
240
+ providerState: { stargate: { guid: `0x${'dd'.repeat(32)}`, sourceEid: 30_184 } },
241
+ transactionHash: `0x${'a5'.repeat(32)}`,
242
+ })
243
+ expect(replacement.type).toBe('registered')
244
+ })
245
+ })
246
+
247
+ describe('complete', () => {
248
+ test('records verified destination evidence and completes once', async () => {
249
+ const record = await Transfer.create(db, createInput())
250
+ await Transfer.registerSourceTransaction(db, {
251
+ chainId: 'eip155:8453',
252
+ id: record.id,
253
+ providerState: { stargate: { guid: `0x${'bb'.repeat(32)}`, sourceEid: 30_184 } },
254
+ transactionHash: `0x${'a3'.repeat(32)}`,
255
+ })
256
+ const options = {
257
+ chainId: 'eip155:4217',
258
+ destinationAmount: TestFunding.transferSnapshot().destinationAmount!,
259
+ id: record.id,
260
+ transactionHash: `0x${'cc'.repeat(32)}`,
261
+ }
262
+ const completed = await Transfer.complete(db, options)
263
+ const replayed = await Transfer.complete(db, options)
264
+ expect({
265
+ completed: completed.type,
266
+ destinationTransactionHashes:
267
+ completed.type === 'completed'
268
+ ? completed.record.snapshot.destinationTransactionHashes
269
+ : undefined,
270
+ replayed: replayed.type,
271
+ status: completed.type === 'completed' ? completed.record.status : undefined,
272
+ version: completed.type === 'completed' ? completed.record.version : undefined,
273
+ }).toMatchInlineSnapshot(`
274
+ {
275
+ "completed": "completed",
276
+ "destinationTransactionHashes": [
277
+ "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
278
+ ],
279
+ "replayed": "replayed",
280
+ "status": "completed",
281
+ "version": 3,
282
+ }
283
+ `)
284
+ })
285
+ })
286
+
132
287
  describe('transition', () => {
133
288
  test('applies a guarded status change and appends an event', async () => {
134
289
  const record = await Transfer.create(db, createInput())