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
@@ -0,0 +1,264 @@
1
+ import { Hash } from 'ox'
2
+ import * as z from 'zod/mini'
3
+
4
+ import * as Db from '../../db/Db.js'
5
+ import * as FundingTransferTransactions from '../../db/tables/fundingTransferTransactions.js'
6
+ import * as FundingTransfers from '../../db/tables/fundingTransfers.js'
7
+ import type * as Metrics from '../../Metrics.js'
8
+ import * as Schema from '../Schema.js'
9
+ import type * as Webhooks from '../Webhooks.js'
10
+ import * as Catalog from './Catalog.js'
11
+ import type * as Provider from './Provider.js'
12
+ import * as Stargate from './providers/stargate.js'
13
+ import * as Transfer from './Transfer.js'
14
+
15
+ const followUpDelaySeconds = 15
16
+ const maximumFollowUpAttempts = 120
17
+ const sweepLimit = 25
18
+
19
+ const providerState = z.object({
20
+ destinationBlockNumber: z.string().check(z.regex(/^\d+$/)),
21
+ routeConfiguration: Stargate.schema.Configuration,
22
+ stargate: z.object({
23
+ guid: Schema.Hash,
24
+ sourceBlock: z.optional(
25
+ z.object({
26
+ hash: Schema.Hash,
27
+ number: z.string().check(z.regex(/^\d+$/)),
28
+ }),
29
+ ),
30
+ sourceEid: z.number().check(z.int(), z.positive()),
31
+ }),
32
+ })
33
+
34
+ /** Queue message that advances one registered funding transfer. */
35
+ export type Message = {
36
+ /** Zero-based destination reconciliation attempt. */
37
+ attempt?: number | undefined
38
+ /** Funding transfer id (`ftr_…`). */
39
+ transferId: string
40
+ /** Funding transfer reconciliation discriminator. */
41
+ type: 'funding:transfer:reconcile'
42
+ }
43
+
44
+ /** Creates a tracker that verifies and completes registered funding transfers. */
45
+ export function createTracker(options: createTracker.Options): createTracker.Tracker {
46
+ const fetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init))
47
+ let sweepCursor: string | undefined
48
+ const record = (outcome: string, count = 1) => {
49
+ options.metrics?.count('funding_transfer_reconciliation_count', count, {
50
+ outcome,
51
+ provider: 'stargate',
52
+ })
53
+ }
54
+
55
+ return {
56
+ async reconcile(message) {
57
+ const db = Db.get(options.db)
58
+ const transfer = await FundingTransfers.get(db, message.transferId)
59
+ if (!transfer || transfer.status === 'completed') {
60
+ record(transfer ? 'already_completed' : 'not_found')
61
+ return { type: 'ignored' }
62
+ }
63
+ if (transfer.status !== 'processing' || transfer.providerId !== 'stargate') {
64
+ record('invalid_state')
65
+ return { type: 'ignored' }
66
+ }
67
+
68
+ const state = providerState.safeParse(transfer.providerState)
69
+ const transactions = await FundingTransferTransactions.listByTransfer(db, transfer.id)
70
+ const source = transactions.find((transaction) => transaction.role === 'source')
71
+ const catalog = await Catalog.read(db)
72
+ const destinationChain = catalog.chainsByKey.get(transfer.snapshot.destinationChain.id)
73
+ const sourceChain = catalog.chainsByKey.get(transfer.snapshot.sourceChain.id)
74
+ const destinationAmount = transfer.snapshot.destinationAmount
75
+ if (
76
+ !state.success ||
77
+ !source ||
78
+ !Hash.validate(source.transactionRef) ||
79
+ !destinationChain ||
80
+ !destinationAmount ||
81
+ !sourceChain ||
82
+ !transfer.snapshot.sender
83
+ )
84
+ throw new EvidenceError('Registered Stargate transfer evidence is incomplete.')
85
+
86
+ const delivery = await Stargate.verifyDestinationDelivery({
87
+ configuration: state.data.routeConfiguration,
88
+ destinationAmount: destinationAmount.baseUnits,
89
+ destinationBlockNumber: state.data.destinationBlockNumber,
90
+ destinationChain,
91
+ destinationTokenAddress: transfer.snapshot.destinationToken.address,
92
+ fetch,
93
+ guid: state.data.stargate.guid,
94
+ recipient: transfer.snapshot.recipient,
95
+ sourceChainId: transfer.snapshot.sourceChain.id,
96
+ sourceTokenAddress: transfer.snapshot.sourceToken.address,
97
+ })
98
+ if (delivery.type === 'invalid')
99
+ throw new EvidenceError('Tempo delivery conflicts with registered transfer terms.')
100
+ if (delivery.type === 'pending') {
101
+ const verified = await Stargate.verifySourceTransaction({
102
+ configuration: state.data.routeConfiguration,
103
+ destinationTokenAddress: transfer.snapshot.destinationToken.address,
104
+ fetch,
105
+ recipient: transfer.snapshot.recipient,
106
+ sender: transfer.snapshot.sender,
107
+ sourceAmount: transfer.snapshot.sourceAmount.baseUnits,
108
+ sourceBlock: state.data.stargate.sourceBlock
109
+ ? {
110
+ hash: state.data.stargate.sourceBlock.hash,
111
+ number: BigInt(state.data.stargate.sourceBlock.number),
112
+ }
113
+ : undefined,
114
+ sourceChain,
115
+ sourceTokenAddress: transfer.snapshot.sourceToken.address,
116
+ transactionHash: source.transactionRef,
117
+ validAfter: transfer.snapshot.quote.sampledAt,
118
+ })
119
+ if (verified.type === 'reorged') {
120
+ const reopened = await Transfer.reopenSourceTransaction(db, {
121
+ chainId: transfer.snapshot.sourceChain.id,
122
+ id: transfer.id,
123
+ providerState: {
124
+ destinationBlockNumber: state.data.destinationBlockNumber,
125
+ routeConfiguration: state.data.routeConfiguration,
126
+ },
127
+ transactionHash: source.transactionRef,
128
+ })
129
+ if (reopened.type !== 'reopened') {
130
+ record('invalid_state')
131
+ return { type: 'ignored' }
132
+ }
133
+ if (options.dispatchTransferUpdates && reopened.references.length > 0)
134
+ await options.dispatchTransferUpdates(reopened.references)
135
+ record('reopened')
136
+ return { type: 'reopened' }
137
+ }
138
+ if (verified.type === 'pending') return followUp(message, options, record)
139
+ if (
140
+ verified.type === 'invalid' ||
141
+ verified.guid.toLowerCase() !== state.data.stargate.guid.toLowerCase() ||
142
+ verified.sourceEid !== state.data.stargate.sourceEid
143
+ )
144
+ throw new EvidenceError('Source transaction conflicts with registered Stargate evidence.')
145
+ return followUp(message, options, record)
146
+ }
147
+
148
+ const completed = await Transfer.complete(db, {
149
+ chainId: transfer.snapshot.destinationChain.id,
150
+ destinationAmount,
151
+ id: transfer.id,
152
+ transactionHash: delivery.transactionHash,
153
+ })
154
+ if (completed.type === 'invalid')
155
+ throw new EvidenceError('Funding transfer cannot accept destination evidence.')
156
+ if (completed.type === 'not_found') {
157
+ record('not_found')
158
+ return { type: 'ignored' }
159
+ }
160
+ if (
161
+ completed.type === 'completed' &&
162
+ options.dispatchTransferUpdates &&
163
+ completed.references.length > 0
164
+ )
165
+ await options.dispatchTransferUpdates(completed.references)
166
+ record(completed.type)
167
+ return { type: 'completed' }
168
+ },
169
+ async tick() {
170
+ if (!options.dispatch) throw new DispatchError()
171
+ const db = Db.get(options.db)
172
+ let records = await FundingTransfers.listProcessing(db, {
173
+ cursor: sweepCursor,
174
+ limit: sweepLimit,
175
+ providerId: 'stargate',
176
+ })
177
+ if (records.length === 0 && sweepCursor !== undefined) {
178
+ sweepCursor = undefined
179
+ records = await FundingTransfers.listProcessing(db, {
180
+ limit: sweepLimit,
181
+ providerId: 'stargate',
182
+ })
183
+ }
184
+ let checked = 0
185
+ for (const transfer of records) {
186
+ await options.dispatch(
187
+ {
188
+ transferId: transfer.id,
189
+ type: 'funding:transfer:reconcile',
190
+ },
191
+ { delaySeconds: 0 },
192
+ )
193
+ sweepCursor = transfer.id
194
+ checked++
195
+ }
196
+ record('swept', checked)
197
+ return { checked }
198
+ },
199
+ }
200
+ }
201
+
202
+ export declare namespace createTracker {
203
+ /** Tracker dependencies and queue dispatch. */
204
+ type Options = {
205
+ /** Authoritative database or per-invocation database factory. */
206
+ db: Db.Source
207
+ /** Enqueues one delayed reconciliation follow-up. */
208
+ dispatch?: ((message: Message, options: { delaySeconds: number }) => Promise<void>) | undefined
209
+ /** Enqueues webhook updates staged with transfer mutations. */
210
+ dispatchTransferUpdates?:
211
+ | ((references: readonly Webhooks.QueueReference[]) => Promise<number>)
212
+ | undefined
213
+ /** Fetch implementation override for chain RPC requests. */
214
+ fetch?: Provider.Fetch | undefined
215
+ /** Optional bounded operational metrics. */
216
+ metrics?: Metrics.Metrics | undefined
217
+ }
218
+
219
+ /** Reconciles one registered funding transfer. */
220
+ type Tracker = {
221
+ /** Verifies or defers one destination delivery. */
222
+ reconcile(message: Message): Promise<Result>
223
+ /** Enqueues processing transfers whose initial dispatch may have failed. */
224
+ tick(): Promise<{ checked: number }>
225
+ }
226
+
227
+ /** One reconciliation disposition. */
228
+ type Result =
229
+ | { type: 'completed' }
230
+ | { type: 'ignored' }
231
+ | { type: 'pending' }
232
+ | { type: 'reopened' }
233
+ }
234
+
235
+ async function followUp(
236
+ message: Message,
237
+ options: createTracker.Options,
238
+ record: (outcome: string) => void,
239
+ ): Promise<createTracker.Result> {
240
+ const attempt = (message.attempt ?? 0) + 1
241
+ if (attempt > maximumFollowUpAttempts) {
242
+ record('exhausted')
243
+ throw new ExhaustedError()
244
+ }
245
+ if (!options.dispatch) throw new DispatchError()
246
+ await options.dispatch(
247
+ { attempt, transferId: message.transferId, type: 'funding:transfer:reconcile' },
248
+ { delaySeconds: followUpDelaySeconds },
249
+ )
250
+ record('pending')
251
+ return { type: 'pending' }
252
+ }
253
+
254
+ class DispatchError extends Error {
255
+ override name = 'Funding.TransferReconciliation.DispatchError'
256
+ }
257
+
258
+ class EvidenceError extends Error {
259
+ override name = 'Funding.TransferReconciliation.EvidenceError'
260
+ }
261
+
262
+ class ExhaustedError extends Error {
263
+ override name = 'Funding.TransferReconciliation.ExhaustedError'
264
+ }
@@ -0,0 +1,128 @@
1
+ import * as TestApp from '../../../test/App.js'
2
+ import * as TestFunding from '../../../test/Funding.js'
3
+ import * as FundingTransferEvents from '../../db/tables/fundingTransferEvents.js'
4
+ import * as Webhooks from '../Webhooks.js'
5
+ import * as Transfer from './Transfer.js'
6
+ import * as TransferWebhook from './TransferWebhook.js'
7
+
8
+ const db = TestApp.database()
9
+
10
+ describe('stage', () => {
11
+ test('stages owner-scoped public versions with stable event ids', async () => {
12
+ const id = Transfer.generateId()
13
+ const orgId = 'org_funding_transfer_webhook'
14
+ const projectId = 'prj_funding_transfer_webhook'
15
+ const org = await subscription({ filters: { id }, orgId })
16
+ const project = await subscription({ filters: { id, status: 'completed' }, orgId, projectId })
17
+ await subscription({ filters: { id }, orgId, projectId: 'prj_funding_transfer_sibling' })
18
+ await subscription({ environment: 'sandbox', filters: { id }, orgId })
19
+ await subscription({ filters: { id: Transfer.generateId() }, orgId })
20
+ await subscription({ filters: { id }, now: new Date('2099-01-01T00:00:00.000Z'), orgId })
21
+
22
+ const created = await db.transaction((tx) =>
23
+ Transfer.createIn(tx, {
24
+ apiKeyId: 'key_funding_transfer_webhook',
25
+ environment: 'production',
26
+ id,
27
+ orgId,
28
+ projectId,
29
+ providerState: { privateRequestId: 'provider-private' },
30
+ snapshot: TestFunding.transferSnapshot(),
31
+ }),
32
+ )
33
+ const processing = await Transfer.transition(db, {
34
+ expectedVersion: created.record.version,
35
+ id,
36
+ status: 'processing',
37
+ })
38
+ if (processing.type !== 'applied') throw new Error('Expected processing transition.')
39
+ const completed = await Transfer.transition(db, {
40
+ expectedVersion: processing.record.version,
41
+ id,
42
+ status: 'completed',
43
+ })
44
+ if (completed.type !== 'applied') throw new Error('Expected completion transition.')
45
+
46
+ expect({
47
+ completed: completed.references.length,
48
+ created: created.references.length,
49
+ processing: processing.references.length,
50
+ }).toEqual({ completed: 2, created: 1, processing: 1 })
51
+
52
+ const rows = await db.kysely
53
+ .selectFrom('webhook_queue_events')
54
+ .select(['envelope', 'subscriptionId'])
55
+ .where('subscriptionId', 'in', [org.id, project.id])
56
+ .orderBy('subscriptionId')
57
+ .execute()
58
+ expect(
59
+ rows.map(({ envelope, subscriptionId }) => ({
60
+ data: envelope.data,
61
+ eventId: envelope.id,
62
+ subscriptionId,
63
+ type: envelope.type,
64
+ })),
65
+ ).toEqual(
66
+ expect.arrayContaining([
67
+ expect.objectContaining({
68
+ data: expect.objectContaining({ id, status: 'awaiting-source', version: 1 }),
69
+ subscriptionId: org.id,
70
+ type: 'funding:transfer.updated',
71
+ }),
72
+ expect.objectContaining({
73
+ data: expect.objectContaining({ id, status: 'processing', version: 2 }),
74
+ subscriptionId: org.id,
75
+ type: 'funding:transfer.updated',
76
+ }),
77
+ expect.objectContaining({
78
+ data: expect.objectContaining({ id, status: 'completed', version: 3 }),
79
+ subscriptionId: org.id,
80
+ type: 'funding:transfer.updated',
81
+ }),
82
+ expect.objectContaining({
83
+ data: expect.objectContaining({ id, status: 'completed', version: 3 }),
84
+ subscriptionId: project.id,
85
+ type: 'funding:transfer.updated',
86
+ }),
87
+ ]),
88
+ )
89
+ expect(rows).toHaveLength(4)
90
+ expect(rows[0]?.envelope.data).not.toHaveProperty('action')
91
+ expect(rows[0]?.envelope.data).not.toHaveProperty('providerState')
92
+
93
+ const events = await FundingTransferEvents.listByTransfer(db, id)
94
+ const repeated = await TransferWebhook.stage(db, {
95
+ changes: [{ event: events.at(-1)!, record: completed.record }],
96
+ })
97
+ expect(repeated.created).toBe(0)
98
+ expect(new Set(repeated.references.map((reference) => reference.eventId))).toEqual(
99
+ new Set([rows.find(({ subscriptionId }) => subscriptionId === project.id)?.envelope.id]),
100
+ )
101
+ })
102
+ })
103
+
104
+ function subscription(options: subscription.Options) {
105
+ return Webhooks.createSubscription(
106
+ db,
107
+ {
108
+ chainId: 4217,
109
+ destination: { type: 'url', url: 'https://example.com/webhooks/tempo' },
110
+ environment: options.environment ?? 'production',
111
+ eventType: 'funding:transfer.updated',
112
+ filters: options.filters,
113
+ owner: { orgId: options.orgId, type: 'api_key' },
114
+ ...(options.projectId === undefined ? {} : { projectId: options.projectId }),
115
+ },
116
+ options.now === undefined ? {} : { now: () => options.now! },
117
+ )
118
+ }
119
+
120
+ declare namespace subscription {
121
+ type Options = {
122
+ environment?: 'production' | 'sandbox' | undefined
123
+ filters: Record<string, unknown>
124
+ now?: Date | undefined
125
+ orgId: string
126
+ projectId?: string | undefined
127
+ }
128
+ }
@@ -0,0 +1,67 @@
1
+ import type * as Db from '../../db/Db.js'
2
+ import type * as FundingTransferEvents from '../../db/tables/fundingTransferEvents.js'
3
+ import type * as FundingTransfers from '../../db/tables/fundingTransfers.js'
4
+ import * as WebhookSubscriptions from '../../db/tables/webhookSubscriptions.js'
5
+ import * as Webhooks from '../Webhooks.js'
6
+
7
+ /** Stages durable webhook obligations for committed funding transfer versions. */
8
+ export async function stage(
9
+ db: Db.Db,
10
+ options: stage.Options,
11
+ ): Promise<Webhooks.ensureQueueEvents.Result> {
12
+ const dispatchables: Webhooks.ensureQueueEvents.Dispatchable[] = []
13
+ const now = new Date().toISOString()
14
+ for (const { event, record } of options.changes) {
15
+ const match = /^eip155:(\d+)$/.exec(event.snapshot.destinationChain.id)
16
+ if (!match?.[1]) continue
17
+ const chainId = Number(match[1])
18
+ if (!Number.isSafeInteger(chainId) || chainId <= 0) continue
19
+ const subscriptions = await WebhookSubscriptions.listActiveForFundingEvent(db, {
20
+ chainId,
21
+ environment: record.environment,
22
+ eventCreatedAt: event.createdAt,
23
+ eventType: 'funding:transfer.updated',
24
+ now,
25
+ orgId: record.orgId,
26
+ ...(record.projectId === null ? {} : { projectId: record.projectId }),
27
+ })
28
+ for (const subscription of subscriptions) {
29
+ if (
30
+ typeof subscription.filters['id'] === 'string' &&
31
+ subscription.filters['id'] !== event.transferId
32
+ )
33
+ continue
34
+ if (
35
+ typeof subscription.filters['status'] === 'string' &&
36
+ subscription.filters['status'] !== event.status
37
+ )
38
+ continue
39
+ dispatchables.push({
40
+ envelope: Webhooks.buildKeyedEnvelope({
41
+ createdAt: new Date(event.createdAt),
42
+ data: event.snapshot,
43
+ key: `${event.transferId}:${event.version}`,
44
+ subscription,
45
+ }),
46
+ subscription,
47
+ })
48
+ }
49
+ }
50
+ return Webhooks.ensureQueueEvents(db, dispatchables)
51
+ }
52
+
53
+ export declare namespace stage {
54
+ /** One committed transfer record and its immutable public event. */
55
+ type Change = {
56
+ /** Immutable public transfer version. */
57
+ event: FundingTransferEvents.Record
58
+ /** Transfer ownership and environment record. */
59
+ record: FundingTransfers.Record
60
+ }
61
+
62
+ /** Committed transfer changes eligible for customer webhook delivery. */
63
+ type Options = {
64
+ /** Transfer changes committed with their delivery obligations. */
65
+ changes: readonly Change[]
66
+ }
67
+ }
@@ -17,6 +17,7 @@ test('exports funding modules', () => {
17
17
  "Subsidy",
18
18
  "Token",
19
19
  "Transfer",
20
+ "TransferReconciliation",
20
21
  ]
21
22
  `)
22
23
  })
@@ -12,3 +12,4 @@ export * as SourceObservation from './SourceObservation.js'
12
12
  export * as Subsidy from './Subsidy.js'
13
13
  export * as Token from './Token.js'
14
14
  export * as Transfer from './Transfer.js'
15
+ export * as TransferReconciliation from './TransferReconciliation.js'
@@ -1,5 +1,4 @@
1
- import { AbiEvent, AbiFunction, Address, Base58, Bytes, Hash, Hex, RpcTransport } from 'ox'
2
- import { createClient, custom, fallback } from 'viem'
1
+ import { AbiEvent, AbiFunction, Address, Base58, Bytes, Hash, Hex } from 'ox'
3
2
  import { getBlockNumber, getLogs, getTransaction, getTransactionReceipt } from 'viem/actions'
4
3
  import * as z from 'zod/mini'
5
4
 
@@ -820,7 +819,7 @@ function createDepositAddressObserver(
820
819
  const stored = schema.DeliveryObservation.safeParse(parameters.providerState)
821
820
  const sourceObservation = await (async () => {
822
821
  if (stored.success) return stored.data.relay
823
- const sourceClient = jsonRpcClient({ fetch: options.fetch, urls: sourceUrls })
822
+ const sourceClient = Viem.createEvmClient({ fetch: options.fetch, urls: sourceUrls })
824
823
  const sourceReceipt = await getTransactionReceipt(sourceClient, {
825
824
  hash: parameters.source.transactionHash as Hex.Hex,
826
825
  })
@@ -868,7 +867,7 @@ function createDepositAddressObserver(
868
867
  })()
869
868
  if (!sourceObservation) return { type: 'pending' }
870
869
 
871
- const destinationClient = jsonRpcClient({ fetch: options.fetch, urls: destinationUrls })
870
+ const destinationClient = Viem.createEvmClient({ fetch: options.fetch, urls: destinationUrls })
872
871
  const destinationHead = await getBlockNumber(destinationClient, { cacheTime: 0 })
873
872
  const destinationLogs = await getLogs(destinationClient, {
874
873
  address: parameters.destinationToken.address as Address.Address,
@@ -1001,17 +1000,6 @@ function decodeEvent<const event extends AbiEvent.AbiEvent>(
1001
1000
  }
1002
1001
  }
1003
1002
 
1004
- function jsonRpcClient(options: { fetch: FundingProvider.Fetch; urls: readonly string[] }) {
1005
- return createClient({
1006
- transport: fallback(
1007
- options.urls.map((url) => {
1008
- const transport = RpcTransport.fromHttp(url, { fetchFn: options.fetch })
1009
- return custom(transport, { retryCount: 0 })
1010
- }),
1011
- ),
1012
- })
1013
- }
1014
-
1015
1003
  function minimum(left: bigint, right: bigint): bigint {
1016
1004
  return left < right ? left : right
1017
1005
  }