tapimo 0.10.2 → 0.11.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 (135) hide show
  1. package/dist/App.d.ts +3412 -1968
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js.map +1 -1
  4. package/dist/Webhooks.d.ts +0 -75
  5. package/dist/Webhooks.d.ts.map +1 -1
  6. package/dist/Webhooks.js +1 -143
  7. package/dist/Webhooks.js.map +1 -1
  8. package/dist/admin/App.d.ts +74 -74
  9. package/dist/admin/apps/api-keys.d.ts +4 -4
  10. package/dist/admin/apps/earn-vaults.d.ts +1 -1
  11. package/dist/admin/apps/organizations.d.ts +3 -3
  12. package/dist/admin/apps/verified-tokens.d.ts +74 -74
  13. package/dist/apps/data/App.d.ts +3618 -2954
  14. package/dist/apps/data/App.d.ts.map +1 -1
  15. package/dist/apps/data/App.js +5 -2
  16. package/dist/apps/data/App.js.map +1 -1
  17. package/dist/apps/data/routes/activities.d.ts +1 -1
  18. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  19. package/dist/apps/data/routes/activities.js +55 -27
  20. package/dist/apps/data/routes/activities.js.map +1 -1
  21. package/dist/apps/data/routes/addresses.d.ts +343 -343
  22. package/dist/apps/data/routes/balances.d.ts +11 -11
  23. package/dist/apps/data/routes/blocks.d.ts +5 -5
  24. package/dist/apps/data/routes/coingecko.d.ts +176 -176
  25. package/dist/apps/data/routes/earn.d.ts +4 -4
  26. package/dist/apps/data/routes/exchanges.d.ts +4 -4
  27. package/dist/apps/data/routes/fee-amm.d.ts +26 -26
  28. package/dist/apps/data/routes/indexer.d.ts +34 -34
  29. package/dist/apps/data/routes/receipts.d.ts +236 -236
  30. package/dist/apps/data/routes/tokenlist.d.ts +12 -12
  31. package/dist/apps/data/routes/tokens.d.ts +3 -3
  32. package/dist/apps/data/routes/transactions.d.ts +1 -1
  33. package/dist/apps/data/routes/transfers.d.ts +13 -13
  34. package/dist/apps/data/routes/valuation.d.ts +21 -21
  35. package/dist/apps/data/routes/verified-tokens.d.ts +5 -5
  36. package/dist/apps/data/routes/webhooks.d.ts +1011 -151
  37. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  38. package/dist/apps/data/routes/webhooks.js +124 -737
  39. package/dist/apps/data/routes/webhooks.js.map +1 -1
  40. package/dist/apps/data/routes/zones.d.ts +31 -31
  41. package/dist/apps/funding/routes/deposit-addresses.d.ts +2 -2
  42. package/dist/apps/funding/routes/providers.d.ts +13 -13
  43. package/dist/apps/funding/routes/transfers.d.ts +33 -33
  44. package/dist/apps/management/App.d.ts +793 -16
  45. package/dist/apps/management/App.d.ts.map +1 -1
  46. package/dist/apps/management/routes/api-keys.d.ts +50 -50
  47. package/dist/apps/management/routes/faucet.d.ts +31 -31
  48. package/dist/apps/management/routes/invite-links.d.ts +42 -42
  49. package/dist/apps/management/routes/members.d.ts +79 -79
  50. package/dist/apps/management/routes/projects.d.ts +40 -40
  51. package/dist/apps/management/routes/usage.d.ts +80 -80
  52. package/dist/apps/management/routes/webhooks.d.ts +1031 -59
  53. package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
  54. package/dist/apps/management/routes/webhooks.js +27 -4
  55. package/dist/apps/management/routes/webhooks.js.map +1 -1
  56. package/dist/db/Db.d.ts.map +1 -1
  57. package/dist/db/Db.js +21 -0
  58. package/dist/db/Db.js.map +1 -1
  59. package/dist/db/Schema.d.ts +7 -1
  60. package/dist/db/Schema.d.ts.map +1 -1
  61. package/dist/db/Schema.js +9 -0
  62. package/dist/db/Schema.js.map +1 -1
  63. package/dist/db/tables/webhookQueueEvents.d.ts +3 -1
  64. package/dist/db/tables/webhookQueueEvents.d.ts.map +1 -1
  65. package/dist/db/tables/webhookQueueEvents.js +4 -0
  66. package/dist/db/tables/webhookQueueEvents.js.map +1 -1
  67. package/dist/db/tables/webhookSubscriptions.d.ts +33 -69
  68. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  69. package/dist/db/tables/webhookSubscriptions.js +63 -100
  70. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  71. package/dist/internal/MetricSink.d.ts +35 -125
  72. package/dist/internal/MetricSink.d.ts.map +1 -1
  73. package/dist/internal/MetricSink.js +50 -121
  74. package/dist/internal/MetricSink.js.map +1 -1
  75. package/dist/internal/Schema.d.ts +3 -1
  76. package/dist/internal/Schema.d.ts.map +1 -1
  77. package/dist/internal/Schema.js +4 -2
  78. package/dist/internal/Schema.js.map +1 -1
  79. package/dist/internal/WebhookTransfer.d.ts +1 -55
  80. package/dist/internal/WebhookTransfer.d.ts.map +1 -1
  81. package/dist/internal/WebhookTransfer.js +1 -239
  82. package/dist/internal/WebhookTransfer.js.map +1 -1
  83. package/dist/internal/Webhooks.d.ts +83 -44
  84. package/dist/internal/Webhooks.d.ts.map +1 -1
  85. package/dist/internal/Webhooks.js +87 -73
  86. package/dist/internal/Webhooks.js.map +1 -1
  87. package/dist/internal/funding/DepositWebhook.d.ts +25 -0
  88. package/dist/internal/funding/DepositWebhook.d.ts.map +1 -0
  89. package/dist/internal/funding/DepositWebhook.js +74 -0
  90. package/dist/internal/funding/DepositWebhook.js.map +1 -0
  91. package/dist/internal/funding/Reconciliation.d.ts +6 -0
  92. package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
  93. package/dist/internal/funding/Reconciliation.js +42 -10
  94. package/dist/internal/funding/Reconciliation.js.map +1 -1
  95. package/dist/internal/funding/index.d.ts +1 -0
  96. package/dist/internal/funding/index.d.ts.map +1 -1
  97. package/dist/internal/funding/index.js +1 -0
  98. package/dist/internal/funding/index.js.map +1 -1
  99. package/dist/internal/webhookDestinations/slack.d.ts.map +1 -1
  100. package/dist/internal/webhookDestinations/slack.js +16 -0
  101. package/dist/internal/webhookDestinations/slack.js.map +1 -1
  102. package/package.json +1 -1
  103. package/src/App.ts +3 -0
  104. package/src/Client.test-d.ts +22 -1
  105. package/src/Webhooks.ts +1 -220
  106. package/src/apps/data/App.ts +8 -2
  107. package/src/apps/data/routes/activities.test.ts +107 -11
  108. package/src/apps/data/routes/activities.ts +64 -32
  109. package/src/apps/data/routes/webhooks.test.ts +179 -1497
  110. package/src/apps/data/routes/webhooks.ts +150 -974
  111. package/src/apps/management/routes/webhooks.test.ts +79 -1
  112. package/src/apps/management/routes/webhooks.ts +40 -4
  113. package/src/db/Db.test.ts +1 -5
  114. package/src/db/Db.ts +23 -0
  115. package/src/db/Schema.ts +13 -0
  116. package/src/db/tables/organizations.test.ts +5 -2
  117. package/src/db/tables/webhookQueueEvents.ts +7 -1
  118. package/src/db/tables/webhookSubscriptions.ts +89 -138
  119. package/src/internal/MetricSink.test.ts +41 -373
  120. package/src/internal/MetricSink.ts +92 -273
  121. package/src/internal/Schema.test.ts +18 -0
  122. package/src/internal/Schema.ts +13 -2
  123. package/src/internal/WebhookTransfer.ts +1 -351
  124. package/src/internal/Webhooks.test.ts +102 -139
  125. package/src/internal/Webhooks.ts +175 -106
  126. package/src/internal/funding/DepositWebhook.test.ts +131 -0
  127. package/src/internal/funding/DepositWebhook.ts +101 -0
  128. package/src/internal/funding/Reconciliation.test.ts +14 -0
  129. package/src/internal/funding/Reconciliation.ts +161 -86
  130. package/src/internal/funding/index.test.ts +1 -0
  131. package/src/internal/funding/index.ts +1 -0
  132. package/src/internal/webhookDestinations/slack.ts +15 -0
  133. package/src/Webhooks.bench.ts +0 -714
  134. package/src/Webhooks.memory.bench.ts +0 -250
  135. package/src/Webhooks.test.ts +0 -146
@@ -0,0 +1,101 @@
1
+ import type * as Db from '../../db/Db.js'
2
+ import * as FundingDeposits from '../../db/tables/fundingDeposits.js'
3
+ import * as WebhookSubscriptions from '../../db/tables/webhookSubscriptions.js'
4
+ import * as Webhooks from '../Webhooks.js'
5
+ import * as Deposit from './Deposit.js'
6
+
7
+ /** Applies a deposit transition and stages every matching webhook obligation atomically. */
8
+ export async function transition(
9
+ db: Db.Db,
10
+ options: Deposit.transition.Options,
11
+ ): Promise<transition.Result> {
12
+ return db.transaction(async (tx) => {
13
+ const result = await Deposit.transition(tx, options)
14
+ if (result.type !== 'applied') return { references: [], result }
15
+ const staged = await stage(tx, { records: [result.record] })
16
+ return { references: staged.references, result }
17
+ })
18
+ }
19
+
20
+ export declare namespace transition {
21
+ /** Transition result plus compact references for atomically staged obligations. */
22
+ type Result = {
23
+ /** Queue references staged with the applied transition. */
24
+ references: Webhooks.QueueReference[]
25
+ /** Version-guarded deposit transition outcome. */
26
+ result: Deposit.transition.Result
27
+ }
28
+ }
29
+
30
+ /** Stages durable webhook obligations for committed funding deposit versions. */
31
+ export async function stage(
32
+ db: Db.Db,
33
+ options: stage.Options,
34
+ ): Promise<Webhooks.ensureQueueEvents.Result> {
35
+ const dispatchables: Webhooks.ensureQueueEvents.Dispatchable[] = []
36
+ const now = new Date().toISOString()
37
+ for (const record of unique(options.records)) {
38
+ const chainId = destinationChainId(record)
39
+ if (chainId === undefined) continue
40
+ const subscriptions = await WebhookSubscriptions.listActiveForFundingDeposit(db, {
41
+ chainId,
42
+ environment: record.environment,
43
+ now,
44
+ orgId: record.orgId,
45
+ ...(record.projectId === null ? {} : { projectId: record.projectId }),
46
+ })
47
+ const data = Deposit.toPublic(record)
48
+ for (const subscription of subscriptions) {
49
+ if (!matches(subscription.filters, data)) continue
50
+ dispatchables.push({
51
+ envelope: Webhooks.buildKeyedEnvelope({
52
+ createdAt: new Date(record.updatedAt),
53
+ data,
54
+ key: `${record.id}:${record.version}`,
55
+ subscription,
56
+ }),
57
+ subscription,
58
+ })
59
+ }
60
+ }
61
+ return Webhooks.ensureQueueEvents(db, dispatchables)
62
+ }
63
+
64
+ export declare namespace stage {
65
+ /** Committed deposit changes eligible for customer webhook delivery. */
66
+ type Options = {
67
+ /** Latest committed funding deposit records. */
68
+ records: readonly FundingDeposits.Record[]
69
+ }
70
+ }
71
+
72
+ function destinationChainId(record: FundingDeposits.Record): number | undefined {
73
+ const match = /^eip155:(\d+)$/.exec(record.snapshot.destinationChain.id)
74
+ if (!match) return undefined
75
+ const chainId = Number(match[1])
76
+ return Number.isSafeInteger(chainId) && chainId > 0 ? chainId : undefined
77
+ }
78
+
79
+ function matches(filters: Record<string, unknown>, deposit: Deposit.Public): boolean {
80
+ if (
81
+ typeof filters['depositAddressId'] === 'string' &&
82
+ filters['depositAddressId'] !== deposit.depositAddressId
83
+ )
84
+ return false
85
+ if (
86
+ typeof filters['recipient'] === 'string' &&
87
+ filters['recipient'].toLowerCase() !== deposit.recipient.toLowerCase()
88
+ )
89
+ return false
90
+ if (typeof filters['status'] === 'string' && filters['status'] !== deposit.status) return false
91
+ return true
92
+ }
93
+
94
+ function unique(records: readonly FundingDeposits.Record[]): FundingDeposits.Record[] {
95
+ const byId = new Map<string, FundingDeposits.Record>()
96
+ for (const record of records) {
97
+ const current = byId.get(record.id)
98
+ if (!current || current.version < record.version) byId.set(record.id, record)
99
+ }
100
+ return [...byId.values()]
101
+ }
@@ -770,9 +770,11 @@ describe('createTracker', () => {
770
770
  ).requests[0]
771
771
  if (!request) throw new Error('Expected a Relay refund request.')
772
772
  const verifyTransfers = Evidence.createVerifier({ db })
773
+ const depositUpdateVersions: number[] = []
773
774
  const first = await Reconciliation.reconcileRequest(db, {
774
775
  address,
775
776
  metrics,
777
+ onDeposit: (deposit) => depositUpdateVersions.push(deposit.version),
776
778
  request,
777
779
  verifyTransfers,
778
780
  })
@@ -784,12 +786,14 @@ describe('createTracker', () => {
784
786
  const second = await Reconciliation.reconcileRequest(db, {
785
787
  address,
786
788
  metrics,
789
+ onDeposit: (deposit) => depositUpdateVersions.push(deposit.version),
787
790
  request,
788
791
  verifyTransfers,
789
792
  })
790
793
  const duplicate = await Reconciliation.reconcileRequest(db, {
791
794
  address,
792
795
  metrics,
796
+ onDeposit: (deposit) => depositUpdateVersions.push(deposit.version),
793
797
  request,
794
798
  verifyTransfers,
795
799
  })
@@ -797,6 +801,9 @@ describe('createTracker', () => {
797
801
  depositAddressId: address.id,
798
802
  sourceTransactionHash,
799
803
  })
804
+ if (!firstRecord || !record) throw new Error('Expected reconciled funding deposits.')
805
+
806
+ expect(depositUpdateVersions).toEqual([2, firstRecord.version, 4, record.version])
800
807
 
801
808
  expect({
802
809
  counts,
@@ -1944,9 +1951,14 @@ describe('createTracker', () => {
1944
1951
  routes: [],
1945
1952
  tokens: [],
1946
1953
  })
1954
+ const dispatchedEventIds: string[][] = []
1947
1955
  const tracker = Reconciliation.createTracker({
1948
1956
  db,
1949
1957
  dispatch: async () => undefined,
1958
+ dispatchDepositUpdates: async (references) => {
1959
+ dispatchedEventIds.push(references.map((reference) => reference.eventId))
1960
+ return references.length
1961
+ },
1950
1962
  providers: [Relay.relay({ apiKey: 'relay-api-key', baseUrl: server.url })],
1951
1963
  verifyTransfers: Evidence.createVerifier({ db }),
1952
1964
  })
@@ -1970,6 +1982,8 @@ describe('createTracker', () => {
1970
1982
  { limit: 10 },
1971
1983
  )
1972
1984
 
1985
+ expect(dispatchedEventIds).toEqual([])
1986
+
1973
1987
  expect({ failures, records: records.map(Deposit.toPublic) }).toMatchInlineSnapshot(
1974
1988
  {
1975
1989
  records: [
@@ -6,7 +6,9 @@ import * as FundingDepositRequestObservations from '../../db/tables/fundingDepos
6
6
  import * as FundingDeposits from '../../db/tables/fundingDeposits.js'
7
7
  import type * as Metrics from '../../Metrics.js'
8
8
  import * as Value from '../Value.js'
9
+ import type * as Webhooks from '../Webhooks.js'
9
10
  import * as Deposit from './Deposit.js'
11
+ import * as DepositWebhook from './DepositWebhook.js'
10
12
  import * as Provider from './Provider.js'
11
13
  import * as Subsidy from './Subsidy.js'
12
14
 
@@ -119,6 +121,17 @@ export function createTracker(options: createTracker.Options): createTracker.Tra
119
121
  })()
120
122
  heartbeat.assert()
121
123
  let created = 0
124
+ const depositWebhookReferences = new Map<string, Webhooks.QueueReference>()
125
+ const onDeposit: NonNullable<reconcileRequest.Options['onDeposit']> = (
126
+ _deposit,
127
+ references,
128
+ ) => {
129
+ for (const reference of references)
130
+ depositWebhookReferences.set(
131
+ `${reference.subscriptionId}:${reference.eventId}`,
132
+ reference,
133
+ )
134
+ }
122
135
  let updated = 0
123
136
  const pendingRequestIds = new Set([
124
137
  ...requests.pendingRequestIds,
@@ -129,6 +142,7 @@ export function createTracker(options: createTracker.Options): createTracker.Tra
129
142
  const result = await reconcileRequest(database, {
130
143
  address: claimed,
131
144
  metrics: options.metrics,
145
+ onDeposit,
132
146
  provider,
133
147
  request,
134
148
  signal: options.signal,
@@ -148,6 +162,7 @@ export function createTracker(options: createTracker.Options): createTracker.Tra
148
162
  const unattributed = await reconcileUnattributed(database, {
149
163
  address: claimed,
150
164
  metrics: options.metrics,
165
+ onDeposit,
151
166
  provider,
152
167
  signal: options.signal,
153
168
  subsidies: options.subsidies,
@@ -166,6 +181,24 @@ export function createTracker(options: createTracker.Options): createTracker.Tra
166
181
  trigger,
167
182
  })
168
183
 
184
+ if (options.dispatchDepositUpdates && depositWebhookReferences.size > 0) {
185
+ const references = [...depositWebhookReferences.values()]
186
+ await options.dispatchDepositUpdates(references).then(
187
+ (count) =>
188
+ options.metrics?.count('funding_deposit_webhook_enqueue_count', count, {
189
+ outcome: 'succeeded',
190
+ provider: claimed.providerId,
191
+ }),
192
+ (cause) => {
193
+ options.metrics?.count('funding_deposit_webhook_enqueue_count', references.length, {
194
+ outcome: 'failed',
195
+ provider: claimed.providerId,
196
+ })
197
+ throw cause
198
+ },
199
+ )
200
+ }
201
+
169
202
  const leaseFailure = await heartbeat.stop()
170
203
  if (leaseFailure !== undefined) throw leaseFailure
171
204
  const completedAt = now()
@@ -295,6 +328,10 @@ export declare namespace createTracker {
295
328
  message: Provider.Webhook.receive.Dispatchable,
296
329
  options?: createTracker.DispatchOptions,
297
330
  ) => Promise<void>
331
+ /** Stages and dispatches committed funding deposit webhook updates. */
332
+ dispatchDepositUpdates?:
333
+ | ((references: readonly Webhooks.QueueReference[]) => Promise<number>)
334
+ | undefined
298
335
  /** Duration of one reconciliation lease. */
299
336
  leaseMs?: number | undefined
300
337
  /** Optional bounded operational metrics sink. */
@@ -589,6 +626,7 @@ type VerifiedDeposit = {
589
626
  type ReconcileDeliveryOptions = {
590
627
  address: FundingDepositAddresses.Record
591
628
  metrics?: Metrics.Metrics | undefined
629
+ onDeposit?: reconcileRequest.Options['onDeposit'] | undefined
592
630
  provider?: Provider.Provider | undefined
593
631
  request?: Provider.listDepositAddressRequests.ReturnType['requests'][number] | undefined
594
632
  signal?: AbortSignal | undefined
@@ -690,7 +728,11 @@ async function reconcileUnattributed(db: Db.Db, options: reconcileUnattributed.O
690
728
  if (!delivery.terminal) terminal = false
691
729
  for (const id of delivery.updatedIds) updatedIds.add(id)
692
730
  }
693
- return { ...(followUp ? { followUp } : {}), terminal, updatedIds }
731
+ return {
732
+ ...(followUp ? { followUp } : {}),
733
+ terminal,
734
+ updatedIds,
735
+ }
694
736
  }
695
737
 
696
738
  declare namespace reconcileUnattributed {
@@ -843,21 +885,25 @@ export async function reconcileRequest(
843
885
  deposit.providerRequestId === null ||
844
886
  !deposit.providerRequestIds.includes(options.request.id)
845
887
  ) {
846
- const result = await Deposit.transition(db, {
847
- expectedVersion: deposit.version,
848
- id: deposit.id,
849
- providerRequestId: options.request.id,
850
- providerRequestIds: unique([...deposit.providerRequestIds, options.request.id]),
851
- providerTransferIndex,
852
- snapshot: {
853
- ...deposit.snapshot,
854
- ...(options.address.snapshot.subsidize ? { destinationAmountRequired } : {}),
855
- ...(transfer.sender ? { sender: transfer.sender } : {}),
856
- sourceAmount,
888
+ const result = await transitionDeposit(
889
+ db,
890
+ {
891
+ expectedVersion: deposit.version,
892
+ id: deposit.id,
893
+ providerRequestId: options.request.id,
894
+ providerRequestIds: unique([...deposit.providerRequestIds, options.request.id]),
895
+ providerTransferIndex,
896
+ snapshot: {
897
+ ...deposit.snapshot,
898
+ ...(options.address.snapshot.subsidize ? { destinationAmountRequired } : {}),
899
+ ...(transfer.sender ? { sender: transfer.sender } : {}),
900
+ sourceAmount,
901
+ },
902
+ sourceTransferIndex: transfer.transferIndex,
903
+ status: deposit.status === 'detected' ? 'bridging' : deposit.status,
857
904
  },
858
- sourceTransferIndex: transfer.transferIndex,
859
- status: deposit.status === 'detected' ? 'bridging' : deposit.status,
860
- })
905
+ options.onDeposit,
906
+ )
861
907
  if (result.type === 'applied') {
862
908
  deposit = result.record
863
909
  updatedIds.add(deposit.id)
@@ -867,7 +913,9 @@ export async function reconcileRequest(
867
913
  }
868
914
  }
869
915
  if (verified.length === 0) return { created, terminal: false, updated: updatedIds.size }
870
- if (!sourceEvidenceComplete) return { created, terminal: false, updated: updatedIds.size }
916
+ if (!sourceEvidenceComplete) {
917
+ return { created, terminal: false, updated: updatedIds.size }
918
+ }
871
919
  const recovery = await reconcileProviderRecovery(db, { ...options, verified })
872
920
  if (recovery) {
873
921
  for (const id of recovery.updatedIds) updatedIds.add(id)
@@ -895,6 +943,10 @@ export declare namespace reconcileRequest {
895
943
  address: FundingDepositAddresses.Record
896
944
  /** Optional bounded operational metrics sink. */
897
945
  metrics?: Metrics.Metrics | undefined
946
+ /** Observes the latest committed form of each verified deposit. */
947
+ onDeposit?:
948
+ | ((deposit: FundingDeposits.Record, references: readonly Webhooks.QueueReference[]) => void)
949
+ | undefined
898
950
  /** Provider integration that discovered the request. */
899
951
  provider?: Provider.Provider | undefined
900
952
  /** Provider request containing discovered chain transactions. */
@@ -922,6 +974,17 @@ export declare namespace reconcileRequest {
922
974
  }
923
975
  }
924
976
 
977
+ async function transitionDeposit(
978
+ db: Db.Db,
979
+ options: Deposit.transition.Options,
980
+ onDeposit: reconcileRequest.Options['onDeposit'],
981
+ ): Promise<Deposit.transition.Result> {
982
+ const committed = await DepositWebhook.transition(db, options)
983
+ if (committed.result.type === 'applied')
984
+ onDeposit?.(committed.result.record, committed.references)
985
+ return committed.result
986
+ }
987
+
925
988
  async function reconcileProviderRecovery(db: Db.Db, options: ReconcileDeliveryOptions) {
926
989
  const failed = (providerFailureStatuses as readonly string[]).includes(
927
990
  options.request?.status ?? '',
@@ -991,15 +1054,19 @@ async function reconcileProviderRecovery(db: Db.Db, options: ReconcileDeliveryOp
991
1054
  if (Deposit.isTerminal(entry.deposit.status)) continue
992
1055
  let deposit = entry.deposit
993
1056
  if (deposit.status !== 'refunding') {
994
- const result = await Deposit.transition(db, {
995
- expectedVersion: deposit.version,
996
- id: deposit.id,
997
- providerTransactionHashes: unique([
998
- ...deposit.providerTransactionHashes,
999
- ...providerRefundTransactionHashes,
1000
- ]),
1001
- status: 'refunding',
1002
- })
1057
+ const result = await transitionDeposit(
1058
+ db,
1059
+ {
1060
+ expectedVersion: deposit.version,
1061
+ id: deposit.id,
1062
+ providerTransactionHashes: unique([
1063
+ ...deposit.providerTransactionHashes,
1064
+ ...providerRefundTransactionHashes,
1065
+ ]),
1066
+ status: 'refunding',
1067
+ },
1068
+ options.onDeposit,
1069
+ )
1003
1070
  if (result.type === 'applied') {
1004
1071
  deposit = result.record
1005
1072
  updatedIds.add(deposit.id)
@@ -1011,20 +1078,24 @@ async function reconcileProviderRecovery(db: Db.Db, options: ReconcileDeliveryOp
1011
1078
  terminal = false
1012
1079
  continue
1013
1080
  }
1014
- const result = await Deposit.transition(db, {
1015
- expectedVersion: deposit.version,
1016
- id: deposit.id,
1017
- snapshot: {
1018
- ...deposit.snapshot,
1019
- refundAmount: Value.tokenAmount({
1020
- baseUnits: amount,
1021
- currency: options.address.snapshot.sourceToken.currency,
1022
- decimals: options.address.snapshot.sourceToken.decimals,
1023
- }),
1024
- refundTransactionHashes,
1081
+ const result = await transitionDeposit(
1082
+ db,
1083
+ {
1084
+ expectedVersion: deposit.version,
1085
+ id: deposit.id,
1086
+ snapshot: {
1087
+ ...deposit.snapshot,
1088
+ refundAmount: Value.tokenAmount({
1089
+ baseUnits: amount,
1090
+ currency: options.address.snapshot.sourceToken.currency,
1091
+ decimals: options.address.snapshot.sourceToken.decimals,
1092
+ }),
1093
+ refundTransactionHashes,
1094
+ },
1095
+ status: 'refunded',
1025
1096
  },
1026
- status: 'refunded',
1027
- })
1097
+ options.onDeposit,
1098
+ )
1028
1099
  if (result.type === 'applied') updatedIds.add(deposit.id)
1029
1100
  else if (result.type !== 'stale' || !Deposit.isTerminal(result.current.status)) terminal = false
1030
1101
  }
@@ -1116,57 +1187,61 @@ async function reconcileDelivery(db: Db.Db, options: ReconcileDeliveryOptions) {
1116
1187
  currency: options.address.snapshot.destinationToken.currency,
1117
1188
  decimals: options.address.snapshot.destinationToken.decimals,
1118
1189
  })
1119
- const result = await Deposit.transition(db, {
1120
- expectedVersion: deposit.version,
1121
- id: deposit.id,
1122
- ...(delivered > 0n
1123
- ? {
1124
- providerOutputAmount: Value.tokenAmount({
1125
- baseUnits: delivered,
1126
- currency: options.address.providerOutputToken.currency,
1127
- decimals: options.address.providerOutputToken.decimals,
1128
- }),
1129
- }
1130
- : {}),
1131
- providerRequestIds: options.request
1132
- ? unique([...deposit.providerRequestIds, options.request.id])
1133
- : deposit.providerRequestIds,
1134
- ...(deliveryObservation?.providerState
1135
- ? {
1136
- providerState: {
1137
- ...deposit.providerState,
1138
- ...deliveryObservation.providerState,
1139
- },
1140
- }
1141
- : {}),
1142
- providerTransactionHashes: unique([
1143
- ...deposit.providerTransactionHashes,
1144
- ...providerTransactionHashes,
1145
- ...(deliveryObservation?.providerTransactionHashes ?? []),
1146
- ]),
1147
- ...(settlement.settlementTransaction
1148
- ? { settlementTransaction: settlement.settlementTransaction }
1149
- : {}),
1150
- ...(settlement.settlementTransactionHash
1151
- ? { settlementTransactionHash: settlement.settlementTransactionHash }
1152
- : {}),
1153
- snapshot: {
1154
- ...deposit.snapshot,
1155
- ...(delivered + settlement.delivered > 0n ? { destinationAmount } : {}),
1156
- ...(settlement.status === 'completed' && !options.address.snapshot.subsidize
1157
- ? { destinationAmountRequired: destinationAmount }
1190
+ const result = await transitionDeposit(
1191
+ db,
1192
+ {
1193
+ expectedVersion: deposit.version,
1194
+ id: deposit.id,
1195
+ ...(delivered > 0n
1196
+ ? {
1197
+ providerOutputAmount: Value.tokenAmount({
1198
+ baseUnits: delivered,
1199
+ currency: options.address.providerOutputToken.currency,
1200
+ decimals: options.address.providerOutputToken.decimals,
1201
+ }),
1202
+ }
1203
+ : {}),
1204
+ providerRequestIds: options.request
1205
+ ? unique([...deposit.providerRequestIds, options.request.id])
1206
+ : deposit.providerRequestIds,
1207
+ ...(deliveryObservation?.providerState
1208
+ ? {
1209
+ providerState: {
1210
+ ...deposit.providerState,
1211
+ ...deliveryObservation.providerState,
1212
+ },
1213
+ }
1158
1214
  : {}),
1159
- destinationTransactionHashes: unique([
1160
- ...(deposit.snapshot.destinationTransactionHashes ?? []),
1161
- ...destinationTransfers.map((evidence) => evidence.transactionHash),
1162
- ...settlement.destinationTransactionHashes,
1215
+ providerTransactionHashes: unique([
1216
+ ...deposit.providerTransactionHashes,
1217
+ ...providerTransactionHashes,
1218
+ ...(deliveryObservation?.providerTransactionHashes ?? []),
1163
1219
  ]),
1220
+ ...(settlement.settlementTransaction
1221
+ ? { settlementTransaction: settlement.settlementTransaction }
1222
+ : {}),
1223
+ ...(settlement.settlementTransactionHash
1224
+ ? { settlementTransactionHash: settlement.settlementTransactionHash }
1225
+ : {}),
1226
+ snapshot: {
1227
+ ...deposit.snapshot,
1228
+ ...(delivered + settlement.delivered > 0n ? { destinationAmount } : {}),
1229
+ ...(settlement.status === 'completed' && !options.address.snapshot.subsidize
1230
+ ? { destinationAmountRequired: destinationAmount }
1231
+ : {}),
1232
+ destinationTransactionHashes: unique([
1233
+ ...(deposit.snapshot.destinationTransactionHashes ?? []),
1234
+ ...destinationTransfers.map((evidence) => evidence.transactionHash),
1235
+ ...settlement.destinationTransactionHashes,
1236
+ ]),
1237
+ },
1238
+ status: settlement.status,
1239
+ ...(settlement.statusReason ? { statusReason: settlement.statusReason } : {}),
1240
+ ...(settlement.subsidyAmount ? { subsidyAmount: settlement.subsidyAmount } : {}),
1241
+ ...(settlement.tempoGasPaid ? { tempoGasPaid: settlement.tempoGasPaid } : {}),
1164
1242
  },
1165
- status: settlement.status,
1166
- ...(settlement.statusReason ? { statusReason: settlement.statusReason } : {}),
1167
- ...(settlement.subsidyAmount ? { subsidyAmount: settlement.subsidyAmount } : {}),
1168
- ...(settlement.tempoGasPaid ? { tempoGasPaid: settlement.tempoGasPaid } : {}),
1169
- })
1243
+ options.onDeposit,
1244
+ )
1170
1245
  if (result.type === 'applied') {
1171
1246
  updatedIds.add(deposit.id)
1172
1247
  if (settlement.broadcast && options.subsidies) {
@@ -8,6 +8,7 @@ test('exports funding modules', () => {
8
8
  "Catalog",
9
9
  "Deposit",
10
10
  "DepositAddress",
11
+ "DepositWebhook",
11
12
  "Evidence",
12
13
  "Provider",
13
14
  "Reconciliation",
@@ -3,6 +3,7 @@ export * as Chain from './Chain.js'
3
3
  export * as Catalog from './Catalog.js'
4
4
  export * as Deposit from './Deposit.js'
5
5
  export * as DepositAddress from './DepositAddress.js'
6
+ export * as DepositWebhook from './DepositWebhook.js'
6
7
  export * as Evidence from './Evidence.js'
7
8
  export * as Provider from './Provider.js'
8
9
  export * as Reconciliation from './Reconciliation.js'
@@ -41,6 +41,7 @@ type Block =
41
41
  /** Per-event emoji + friendly header name; also the notification fallback lead. */
42
42
  const friendlyName: Record<Webhooks.Envelope['type'], string> = {
43
43
  'block:created': ':link: Block created',
44
+ 'funding:deposit.updated': ':inbox_tray: Funding deposit updated',
44
45
  'log:emitted': ':label: Log emitted',
45
46
  ping: ':bell: Webhook test ping',
46
47
  'token:transfer': ':money_with_wings: Token transfer',
@@ -150,9 +151,23 @@ function renderEvent(
150
151
  if (type === 'transaction:included') return transactionLayout(data, base)
151
152
  if (type === 'log:emitted') return logLayout(data, base)
152
153
  if (type === 'block:created') return blockLayout(data, base)
154
+ if (type === 'funding:deposit.updated') return fundingDepositLayout(data)
153
155
  return pingLayout()
154
156
  }
155
157
 
158
+ function fundingDepositLayout(data: Record<string, unknown>): Layout {
159
+ const fields: Field[] = []
160
+ field(fields, 'Deposit', escape(str(data['id']) ?? 'unknown'))
161
+ field(fields, 'Status', escape(str(data['status']) ?? 'unknown'))
162
+ const recipient = str(data['recipient'])
163
+ if (recipient !== undefined) field(fields, 'Recipient', escape(recipient))
164
+ return {
165
+ fields,
166
+ header: friendlyName['funding:deposit.updated'],
167
+ text: `Funding deposit updated · ${str(data['status']) ?? 'unknown'}`,
168
+ }
169
+ }
170
+
156
171
  function transferLayout(data: Record<string, unknown>, base: string | undefined): Layout {
157
172
  const amount = str(data['amount'])
158
173
  const token =