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.
- package/dist/App.d.ts +6064 -4829
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +1 -0
- package/dist/App.js.map +1 -1
- package/dist/Webhooks.js +2 -2
- package/dist/Webhooks.js.map +1 -1
- package/dist/admin/App.d.ts +1008 -917
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/earn-vaults.d.ts +48 -0
- package/dist/admin/apps/earn-vaults.d.ts.map +1 -1
- package/dist/admin/apps/earn-vaults.js +2 -1
- package/dist/admin/apps/earn-vaults.js.map +1 -1
- package/dist/admin/apps/organizations.d.ts +92 -3
- package/dist/admin/apps/organizations.d.ts.map +1 -1
- package/dist/admin/apps/organizations.js +69 -7
- package/dist/admin/apps/organizations.js.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +74 -74
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.js +69 -69
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/apps/Zones.d.ts +12 -12
- package/dist/apps/Zones.js +3 -3
- package/dist/apps/data/App.d.ts +2909 -2523
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +11 -6
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/routes/addresses.d.ts +22 -22
- package/dist/apps/data/routes/earn.d.ts +183 -7
- package/dist/apps/data/routes/earn.d.ts.map +1 -1
- package/dist/apps/data/routes/earn.js +434 -75
- package/dist/apps/data/routes/earn.js.map +1 -1
- package/dist/apps/data/routes/exchanges.d.ts +15 -0
- package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
- package/dist/apps/data/routes/exchanges.js +180 -114
- package/dist/apps/data/routes/exchanges.js.map +1 -1
- package/dist/apps/data/routes/fee-amm.d.ts.map +1 -1
- package/dist/apps/data/routes/fee-amm.js +18 -19
- package/dist/apps/data/routes/fee-amm.js.map +1 -1
- package/dist/apps/data/routes/tokens.d.ts +93 -93
- package/dist/apps/data/routes/transfers.d.ts +12 -12
- package/dist/apps/data/routes/valuation.d.ts +30 -30
- package/dist/apps/data/routes/verified-tokens.d.ts +5 -5
- package/dist/apps/data/routes/webhooks.d.ts +805 -102
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +47 -5
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/funding/App.d.ts +456 -45
- package/dist/apps/funding/App.d.ts.map +1 -1
- package/dist/apps/funding/App.js +1 -1
- package/dist/apps/funding/App.js.map +1 -1
- package/dist/apps/funding/routes/deposit-addresses.d.ts +17 -17
- package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
- package/dist/apps/funding/routes/deposit-addresses.js +1 -1
- package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
- package/dist/apps/funding/routes/transfers.d.ts +654 -73
- package/dist/apps/funding/routes/transfers.d.ts.map +1 -1
- package/dist/apps/funding/routes/transfers.js +211 -5
- package/dist/apps/funding/routes/transfers.js.map +1 -1
- package/dist/apps/management/App.d.ts +831 -387
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/Billing.d.ts +1 -1
- package/dist/apps/management/Billing.d.ts.map +1 -1
- package/dist/apps/management/Billing.js +17 -1
- package/dist/apps/management/Billing.js.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +76 -76
- package/dist/apps/management/routes/billing.d.ts +76 -76
- package/dist/apps/management/routes/faucet.d.ts +12 -12
- package/dist/apps/management/routes/invitations.d.ts +24 -24
- package/dist/apps/management/routes/invite-links.d.ts +46 -46
- package/dist/apps/management/routes/members.d.ts +46 -46
- package/dist/apps/management/routes/orgs.d.ts +20 -20
- package/dist/apps/management/routes/projects.d.ts +44 -44
- package/dist/apps/management/routes/usage.d.ts +32 -32
- package/dist/apps/management/routes/verified-token-requests.d.ts +16 -16
- package/dist/apps/management/routes/webhooks.d.ts +707 -57
- package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/management/routes/webhooks.js +21 -21
- package/dist/apps/management/routes/webhooks.js.map +1 -1
- package/dist/apps/relay/App.d.ts.map +1 -1
- package/dist/apps/relay/App.js +115 -23
- package/dist/apps/relay/App.js.map +1 -1
- package/dist/apps/relay/Sponsorships.d.ts.map +1 -1
- package/dist/apps/relay/Sponsorships.js +27 -1
- package/dist/apps/relay/Sponsorships.js.map +1 -1
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +22 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +8 -3
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +16 -1
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/fundingTransferTransactions.d.ts +38 -0
- package/dist/db/tables/fundingTransferTransactions.d.ts.map +1 -1
- package/dist/db/tables/fundingTransferTransactions.js +44 -3
- package/dist/db/tables/fundingTransferTransactions.js.map +1 -1
- package/dist/db/tables/fundingTransfers.d.ts +17 -0
- package/dist/db/tables/fundingTransfers.d.ts.map +1 -1
- package/dist/db/tables/fundingTransfers.js +21 -0
- package/dist/db/tables/fundingTransfers.js.map +1 -1
- package/dist/db/tables/organizations.d.ts +24 -0
- package/dist/db/tables/organizations.d.ts.map +1 -1
- package/dist/db/tables/organizations.js +27 -0
- package/dist/db/tables/organizations.js.map +1 -1
- package/dist/db/tables/projects.d.ts +25 -1
- package/dist/db/tables/projects.d.ts.map +1 -1
- package/dist/db/tables/projects.js +26 -0
- package/dist/db/tables/projects.js.map +1 -1
- package/dist/db/tables/sponsoredTransactions.d.ts +44 -0
- package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
- package/dist/db/tables/sponsoredTransactions.js +146 -13
- package/dist/db/tables/sponsoredTransactions.js.map +1 -1
- package/dist/db/tables/webhookSubscriptions.d.ts +10 -6
- package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
- package/dist/db/tables/webhookSubscriptions.js +9 -6
- package/dist/db/tables/webhookSubscriptions.js.map +1 -1
- package/dist/internal/EarnVaults.js +5 -5
- package/dist/internal/EarnVaults.js.map +1 -1
- package/dist/internal/Log.d.ts +1 -1
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js.map +1 -1
- package/dist/internal/Viem.d.ts +16 -1
- package/dist/internal/Viem.d.ts.map +1 -1
- package/dist/internal/Viem.js +12 -2
- package/dist/internal/Viem.js.map +1 -1
- package/dist/internal/funding/DepositWebhook.d.ts.map +1 -1
- package/dist/internal/funding/DepositWebhook.js +2 -1
- package/dist/internal/funding/DepositWebhook.js.map +1 -1
- package/dist/internal/funding/Evidence.d.ts.map +1 -1
- package/dist/internal/funding/Evidence.js +2 -10
- package/dist/internal/funding/Evidence.js.map +1 -1
- package/dist/internal/funding/Provider.d.ts +40 -0
- package/dist/internal/funding/Provider.d.ts.map +1 -1
- package/dist/internal/funding/Provider.js +5 -0
- package/dist/internal/funding/Provider.js.map +1 -1
- package/dist/internal/funding/SourceObservation.d.ts.map +1 -1
- package/dist/internal/funding/SourceObservation.js +5 -7
- package/dist/internal/funding/SourceObservation.js.map +1 -1
- package/dist/internal/funding/Transfer.d.ts +101 -6
- package/dist/internal/funding/Transfer.d.ts.map +1 -1
- package/dist/internal/funding/Transfer.js +162 -34
- package/dist/internal/funding/Transfer.js.map +1 -1
- package/dist/internal/funding/TransferReconciliation.d.ts +52 -0
- package/dist/internal/funding/TransferReconciliation.d.ts.map +1 -0
- package/dist/internal/funding/TransferReconciliation.js +192 -0
- package/dist/internal/funding/TransferReconciliation.js.map +1 -0
- package/dist/internal/funding/TransferWebhook.d.ts +21 -0
- package/dist/internal/funding/TransferWebhook.d.ts.map +1 -0
- package/dist/internal/funding/TransferWebhook.js +43 -0
- package/dist/internal/funding/TransferWebhook.js.map +1 -0
- package/dist/internal/funding/index.d.ts +1 -0
- package/dist/internal/funding/index.d.ts.map +1 -1
- package/dist/internal/funding/index.js +1 -0
- package/dist/internal/funding/index.js.map +1 -1
- package/dist/internal/funding/providers/relay.d.ts.map +1 -1
- package/dist/internal/funding/providers/relay.js +3 -12
- package/dist/internal/funding/providers/relay.js.map +1 -1
- package/dist/internal/funding/providers/stargate.d.ts +99 -0
- package/dist/internal/funding/providers/stargate.d.ts.map +1 -1
- package/dist/internal/funding/providers/stargate.js +337 -35
- package/dist/internal/funding/providers/stargate.js.map +1 -1
- package/dist/internal/webhookDestinations/slack.d.ts.map +1 -1
- package/dist/internal/webhookDestinations/slack.js +16 -0
- package/dist/internal/webhookDestinations/slack.js.map +1 -1
- package/package.json +1 -1
- package/src/App.ts +1 -0
- package/src/Client.test-d.ts +83 -2
- package/src/Webhooks.ts +2 -2
- package/src/admin/apps/earn-vaults.ts +2 -1
- package/src/admin/apps/organizations.test.ts +53 -1
- package/src/admin/apps/organizations.ts +90 -5
- package/src/admin/ui/src/lib/organizations.test.ts +112 -1
- package/src/admin/ui/src/lib/organizations.ts +55 -0
- package/src/admin/ui/src/routes/organizations_.$orgId.tsx +116 -0
- package/src/admin/ui.generated.ts +69 -69
- package/src/apps/Zones.test.ts +3 -3
- package/src/apps/Zones.ts +3 -3
- package/src/apps/composition.test.ts +1 -0
- package/src/apps/data/App.ts +15 -6
- package/src/apps/data/routes/earn.test.ts +369 -14
- package/src/apps/data/routes/earn.ts +548 -79
- package/src/apps/data/routes/exchanges.test.ts +740 -15
- package/src/apps/data/routes/exchanges.ts +954 -753
- package/src/apps/data/routes/fee-amm.ts +17 -18
- package/src/apps/data/routes/webhooks.test.ts +21 -5
- package/src/apps/data/routes/webhooks.ts +63 -8
- package/src/apps/funding/App.ts +12 -2
- package/src/apps/funding/routes/deposit-addresses.test.ts +4 -4
- package/src/apps/funding/routes/deposit-addresses.ts +2 -2
- package/src/apps/funding/routes/transfers.test.ts +510 -19
- package/src/apps/funding/routes/transfers.ts +271 -5
- package/src/apps/management/Billing.ts +18 -5
- package/src/apps/management/routes/webhooks.test.ts +3 -3
- package/src/apps/management/routes/webhooks.ts +24 -25
- package/src/apps/relay/App.test.ts +212 -1
- package/src/apps/relay/App.ts +158 -29
- package/src/apps/relay/Sponsorships.ts +24 -1
- package/src/db/Db.ts +35 -0
- package/src/db/Schema.ts +17 -2
- package/src/db/tables/fundingTransferTransactions.test.ts +43 -0
- package/src/db/tables/fundingTransferTransactions.ts +79 -3
- package/src/db/tables/fundingTransfers.test.ts +26 -0
- package/src/db/tables/fundingTransfers.ts +36 -0
- package/src/db/tables/organizations.test.ts +41 -0
- package/src/db/tables/organizations.ts +49 -0
- package/src/db/tables/projects.test.ts +55 -0
- package/src/db/tables/projects.ts +46 -1
- package/src/db/tables/sponsoredTransactions.test.ts +338 -0
- package/src/db/tables/sponsoredTransactions.ts +197 -13
- package/src/db/tables/webhookSubscriptions.ts +18 -11
- package/src/internal/EarnVaults.ts +5 -5
- package/src/internal/Log.test-d.ts +2 -0
- package/src/internal/Log.ts +2 -0
- package/src/internal/Viem.ts +39 -3
- package/src/internal/Webhooks.test.ts +45 -0
- package/src/internal/funding/DepositWebhook.ts +2 -1
- package/src/internal/funding/Evidence.ts +2 -13
- package/src/internal/funding/Provider.ts +43 -0
- package/src/internal/funding/SourceObservation.ts +5 -9
- package/src/internal/funding/Transfer.test.ts +155 -0
- package/src/internal/funding/Transfer.ts +254 -37
- package/src/internal/funding/TransferReconciliation.test.ts +147 -0
- package/src/internal/funding/TransferReconciliation.ts +264 -0
- package/src/internal/funding/TransferWebhook.test.ts +128 -0
- package/src/internal/funding/TransferWebhook.ts +67 -0
- package/src/internal/funding/index.test.ts +1 -0
- package/src/internal/funding/index.ts +1 -0
- package/src/internal/funding/providers/relay.ts +3 -15
- package/src/internal/funding/providers/stargate.test.ts +169 -4
- package/src/internal/funding/providers/stargate.ts +498 -51
- 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
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { AbiEvent, AbiFunction, Address, Base58, Bytes, Hash, Hex
|
|
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 =
|
|
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 =
|
|
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
|
}
|