tapimo 0.7.2 → 0.9.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/README.md +10 -7
- package/dist/ApiKey.d.ts +3 -0
- package/dist/ApiKey.d.ts.map +1 -1
- package/dist/ApiKey.js +10 -0
- package/dist/ApiKey.js.map +1 -1
- package/dist/ApiKeys.d.ts +7 -3
- package/dist/ApiKeys.d.ts.map +1 -1
- package/dist/ApiKeys.js +9 -2
- package/dist/ApiKeys.js.map +1 -1
- package/dist/App.d.ts +20117 -19183
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +61 -4
- package/dist/App.js.map +1 -1
- package/dist/admin/App.d.ts +84 -74
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +14 -1
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.js +6 -2
- package/dist/admin/apps/api-keys.js.map +1 -1
- package/dist/admin/apps/organizations.d.ts +2 -0
- package/dist/admin/apps/organizations.d.ts.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +11 -11
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.js +67 -67
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/analytics/Analytics.d.ts +2 -0
- package/dist/analytics/Analytics.d.ts.map +1 -1
- package/dist/analytics/Analytics.js +3 -0
- package/dist/analytics/Analytics.js.map +1 -1
- package/dist/apps/data/App.d.ts +11696 -13937
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +4 -8
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/routes/activities.d.ts +271 -271
- package/dist/apps/data/routes/addresses.d.ts +398 -167
- package/dist/apps/data/routes/addresses.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.d.ts +278 -8
- package/dist/apps/data/routes/balances.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.js +114 -2
- package/dist/apps/data/routes/balances.js.map +1 -1
- package/dist/apps/data/routes/blocks.d.ts +123 -123
- package/dist/apps/data/routes/coingecko.d.ts +170 -170
- package/dist/apps/data/routes/earn.d.ts +498 -6
- package/dist/apps/data/routes/earn.d.ts.map +1 -1
- package/dist/apps/data/routes/earn.js +876 -4
- package/dist/apps/data/routes/earn.js.map +1 -1
- package/dist/apps/data/routes/exchanges.d.ts +10 -10
- package/dist/apps/data/routes/fee-amm.d.ts +2 -2
- package/dist/apps/data/routes/indexer.d.ts +6 -6
- package/dist/apps/data/routes/indexer.js +4 -1
- package/dist/apps/data/routes/indexer.js.map +1 -1
- package/dist/apps/data/routes/receipts.d.ts +235 -235
- package/dist/apps/data/routes/rpc.d.ts +31 -31
- package/dist/apps/data/routes/rpc.js +4 -1
- package/dist/apps/data/routes/rpc.js.map +1 -1
- package/dist/apps/data/routes/tokenlist.d.ts +36 -36
- package/dist/apps/data/routes/tokens.d.ts +449 -449
- package/dist/apps/data/routes/transactions.d.ts +2 -2
- package/dist/apps/data/routes/transfers.d.ts +81 -81
- package/dist/apps/data/routes/valuation.d.ts +1 -1
- package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
- package/dist/apps/data/routes/webhooks.d.ts +9 -9
- package/dist/apps/data/routes/zones.d.ts +14 -14
- package/dist/apps/funding/Access.d.ts +16 -0
- package/dist/apps/funding/Access.d.ts.map +1 -0
- package/dist/apps/funding/Access.js +22 -0
- package/dist/apps/funding/Access.js.map +1 -0
- package/dist/apps/{data/routes/funding.d.ts → funding/App.d.ts} +1640 -3217
- package/dist/apps/funding/App.d.ts.map +1 -0
- package/dist/apps/funding/App.js +61 -0
- package/dist/apps/funding/App.js.map +1 -0
- package/dist/apps/funding/Inventory.d.ts +5 -0
- package/dist/apps/funding/Inventory.d.ts.map +1 -0
- package/dist/apps/funding/Inventory.js +25 -0
- package/dist/apps/funding/Inventory.js.map +1 -0
- package/dist/apps/funding/ProviderAttempts.d.ts +5 -0
- package/dist/apps/funding/ProviderAttempts.d.ts.map +1 -0
- package/dist/apps/funding/ProviderAttempts.js +24 -0
- package/dist/apps/funding/ProviderAttempts.js.map +1 -0
- package/dist/apps/funding/index.d.ts +2 -0
- package/dist/apps/funding/index.d.ts.map +1 -0
- package/dist/apps/funding/index.js +2 -0
- package/dist/apps/funding/index.js.map +1 -0
- package/dist/apps/funding/routes/chains.d.ts +169 -0
- package/dist/apps/funding/routes/chains.d.ts.map +1 -0
- package/dist/apps/funding/routes/chains.js +80 -0
- package/dist/apps/funding/routes/chains.js.map +1 -0
- package/dist/apps/funding/routes/deposit-addresses.d.ts +1997 -0
- package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -0
- package/dist/apps/funding/routes/deposit-addresses.js +778 -0
- package/dist/apps/funding/routes/deposit-addresses.js.map +1 -0
- package/dist/apps/funding/routes/providers.d.ts +91 -0
- package/dist/apps/funding/routes/providers.d.ts.map +1 -0
- package/dist/apps/funding/routes/providers.js +56 -0
- package/dist/apps/funding/routes/providers.js.map +1 -0
- package/dist/apps/funding/routes/quotes.d.ts +426 -0
- package/dist/apps/funding/routes/quotes.d.ts.map +1 -0
- package/dist/apps/funding/routes/quotes.js +188 -0
- package/dist/apps/funding/routes/quotes.js.map +1 -0
- package/dist/apps/funding/routes/transfers.d.ts +1999 -0
- package/dist/apps/funding/routes/transfers.d.ts.map +1 -0
- package/dist/apps/funding/routes/transfers.js +614 -0
- package/dist/apps/funding/routes/transfers.js.map +1 -0
- package/dist/apps/index.d.ts +1 -0
- package/dist/apps/index.d.ts.map +1 -1
- package/dist/apps/index.js +1 -0
- package/dist/apps/index.js.map +1 -1
- package/dist/apps/management/App.d.ts +430 -165
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +298 -21
- package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.js +75 -1
- package/dist/apps/management/routes/api-keys.js.map +1 -1
- package/dist/apps/management/routes/billing.d.ts +6 -6
- package/dist/apps/management/routes/faucet.d.ts +35 -35
- package/dist/apps/management/routes/invitations.d.ts +122 -122
- package/dist/apps/management/routes/invite-links.d.ts +38 -38
- package/dist/apps/management/routes/members.d.ts +69 -69
- package/dist/apps/management/routes/orgs.d.ts +99 -99
- package/dist/apps/management/routes/projects.d.ts +25 -25
- package/dist/apps/management/routes/scopes.d.ts +1 -1
- package/dist/apps/management/routes/usage.d.ts +72 -72
- package/dist/apps/management/routes/verified-token-requests.d.ts +77 -77
- package/dist/apps/management/routes/webhooks.d.ts +128 -128
- package/dist/apps/mcp.d.ts +2 -3
- package/dist/apps/mcp.d.ts.map +1 -1
- package/dist/apps/mcp.js +16 -14
- package/dist/apps/mcp.js.map +1 -1
- package/dist/apps/metadata.d.ts +4 -0
- package/dist/apps/metadata.d.ts.map +1 -1
- package/dist/apps/metadata.js +10 -0
- package/dist/apps/metadata.js.map +1 -1
- package/dist/apps/mpp/App.d.ts +44 -44
- package/dist/cloudflare.d.ts +50 -0
- package/dist/cloudflare.d.ts.map +1 -1
- package/dist/cloudflare.js +178 -0
- package/dist/cloudflare.js.map +1 -1
- package/dist/db/Db.d.ts +2 -0
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +45 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +12 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +20 -0
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/fundingDepositAddresses.d.ts +13 -0
- package/dist/db/tables/fundingDepositAddresses.d.ts.map +1 -1
- package/dist/db/tables/fundingDepositAddresses.js +12 -0
- package/dist/db/tables/fundingDepositAddresses.js.map +1 -1
- package/dist/db/tables/fundingDepositRequestObservations.d.ts +45 -0
- package/dist/db/tables/fundingDepositRequestObservations.d.ts.map +1 -0
- package/dist/db/tables/fundingDepositRequestObservations.js +46 -0
- package/dist/db/tables/fundingDepositRequestObservations.js.map +1 -0
- package/dist/db/tables/fundingDeposits.d.ts +74 -1
- package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
- package/dist/db/tables/fundingDeposits.js +126 -0
- package/dist/db/tables/fundingDeposits.js.map +1 -1
- package/dist/internal/Auth.d.ts +15 -11
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +52 -17
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/Cache.js +1 -1
- package/dist/internal/Cache.js.map +1 -1
- package/dist/internal/EdgeCache.d.ts +2 -2
- package/dist/internal/EdgeCache.d.ts.map +1 -1
- package/dist/internal/EdgeCache.js +10 -4
- package/dist/internal/EdgeCache.js.map +1 -1
- package/dist/internal/IpAllowlist.d.ts +15 -0
- package/dist/internal/IpAllowlist.d.ts.map +1 -0
- package/dist/internal/IpAllowlist.js +27 -0
- package/dist/internal/IpAllowlist.js.map +1 -0
- package/dist/internal/OpenApi.d.ts +1 -1
- package/dist/internal/OpenApi.js +1 -1
- package/dist/internal/OpenApi.js.map +1 -1
- package/dist/internal/Tidx.d.ts +6 -0
- package/dist/internal/Tidx.d.ts.map +1 -1
- package/dist/internal/Tidx.js +13 -3
- package/dist/internal/Tidx.js.map +1 -1
- package/dist/internal/Viem.d.ts +8 -0
- package/dist/internal/Viem.d.ts.map +1 -1
- package/dist/internal/Viem.js +12 -5
- package/dist/internal/Viem.js.map +1 -1
- package/dist/internal/funding/Deposit.d.ts +42 -0
- package/dist/internal/funding/Deposit.d.ts.map +1 -1
- package/dist/internal/funding/Deposit.js +37 -5
- package/dist/internal/funding/Deposit.js.map +1 -1
- package/dist/internal/funding/Evidence.d.ts +1 -5
- package/dist/internal/funding/Evidence.d.ts.map +1 -1
- package/dist/internal/funding/Evidence.js +4 -3
- package/dist/internal/funding/Evidence.js.map +1 -1
- package/dist/internal/funding/Provider.d.ts +102 -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/Reconciliation.d.ts +16 -2
- package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
- package/dist/internal/funding/Reconciliation.js +348 -54
- package/dist/internal/funding/Reconciliation.js.map +1 -1
- package/dist/internal/funding/SourceObservation.d.ts +108 -0
- package/dist/internal/funding/SourceObservation.d.ts.map +1 -0
- package/dist/internal/funding/SourceObservation.js +143 -0
- package/dist/internal/funding/SourceObservation.js.map +1 -0
- package/dist/internal/funding/Subsidy.d.ts +2 -2
- package/dist/internal/funding/Subsidy.d.ts.map +1 -1
- package/dist/internal/funding/Subsidy.js +1 -1
- package/dist/internal/funding/Subsidy.js.map +1 -1
- 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 +43 -1
- package/dist/internal/funding/providers/relay.d.ts.map +1 -1
- package/dist/internal/funding/providers/relay.js +438 -27
- package/dist/internal/funding/providers/relay.js.map +1 -1
- package/package.json +3 -3
- package/src/ApiKey.test.ts +35 -0
- package/src/ApiKey.ts +27 -0
- package/src/ApiKeys.test.ts +29 -0
- package/src/ApiKeys.ts +11 -3
- package/src/App.test.ts +55 -5
- package/src/App.ts +79 -5
- package/src/Client.test-d.ts +203 -28
- package/src/admin/apps/api-keys.test.ts +25 -1
- package/src/admin/apps/api-keys.ts +6 -2
- package/src/admin/ui.generated.ts +67 -67
- package/src/analytics/Analytics.ts +4 -0
- package/src/apps/composition.test.ts +130 -3
- package/src/apps/data/App.test.ts +0 -12
- package/src/apps/data/App.ts +4 -8
- package/src/apps/data/routes/balances.test.ts +42 -0
- package/src/apps/data/routes/balances.ts +218 -85
- package/src/apps/data/routes/earn.test.ts +363 -0
- package/src/apps/data/routes/earn.ts +1141 -3
- package/src/apps/data/routes/indexer.ts +2 -1
- package/src/apps/data/routes/rpc.ts +2 -0
- package/src/apps/data/routes/tokens.test.ts +6 -8
- package/src/apps/funding/Access.ts +26 -0
- package/src/apps/funding/App.test.ts +78 -0
- package/src/apps/funding/App.ts +78 -0
- package/src/apps/funding/Inventory.ts +30 -0
- package/src/apps/funding/ProviderAttempts.ts +26 -0
- package/src/apps/funding/index.ts +1 -0
- package/src/apps/funding/routes/chains.test.ts +78 -0
- package/src/apps/funding/routes/chains.ts +101 -0
- package/src/apps/funding/routes/deposit-addresses.test.ts +1649 -0
- package/src/apps/funding/routes/deposit-addresses.ts +932 -0
- package/src/apps/funding/routes/providers.test.ts +165 -0
- package/src/apps/funding/routes/providers.ts +73 -0
- package/src/apps/funding/routes/quotes.test.ts +511 -0
- package/src/apps/funding/routes/quotes.ts +205 -0
- package/src/apps/funding/routes/transfers.test.ts +1236 -0
- package/src/apps/funding/routes/transfers.ts +704 -0
- package/src/apps/index.ts +1 -0
- package/src/apps/management/routes/api-keys.test.ts +98 -0
- package/src/apps/management/routes/api-keys.ts +96 -1
- package/src/apps/mcp.test.ts +17 -2
- package/src/apps/mcp.ts +18 -13
- package/src/apps/metadata.ts +12 -0
- package/src/apps/mpp/internal/Policy.test.ts +7 -1
- package/src/cloudflare.ts +279 -0
- package/src/db/Db.ts +71 -0
- package/src/db/Schema.ts +24 -0
- package/src/db/tables/fundingDepositAddresses.ts +28 -0
- package/src/db/tables/fundingDepositRequestObservations.test.ts +106 -0
- package/src/db/tables/fundingDepositRequestObservations.ts +99 -0
- package/src/db/tables/fundingDeposits.test.ts +239 -0
- package/src/db/tables/fundingDeposits.ts +214 -1
- package/src/internal/Auth.test.ts +205 -5
- package/src/internal/Auth.ts +63 -19
- package/src/internal/Cache.test.ts +37 -3
- package/src/internal/Cache.ts +1 -1
- package/src/internal/EdgeCache.test.ts +7 -7
- package/src/internal/EdgeCache.ts +13 -4
- package/src/internal/IpAllowlist.test.ts +52 -0
- package/src/internal/IpAllowlist.ts +39 -0
- package/src/internal/Log.test.ts +7 -1
- package/src/internal/OpenApi.ts +1 -1
- package/src/internal/Tidx.test.ts +28 -0
- package/src/internal/Tidx.ts +19 -3
- package/src/internal/Viem.test.ts +28 -0
- package/src/internal/Viem.ts +21 -5
- package/src/internal/funding/Deposit.ts +74 -5
- package/src/internal/funding/Evidence.test.ts +2 -2
- package/src/internal/funding/Evidence.ts +4 -9
- package/src/internal/funding/Provider.ts +115 -0
- package/src/internal/funding/Reconciliation.local.test.ts +7 -0
- package/src/internal/funding/Reconciliation.test.ts +1364 -109
- package/src/internal/funding/Reconciliation.ts +467 -88
- package/src/internal/funding/SourceObservation.test.ts +301 -0
- package/src/internal/funding/SourceObservation.ts +267 -0
- package/src/internal/funding/Subsidy.ts +2 -2
- package/src/internal/funding/index.test.ts +1 -0
- package/src/internal/funding/index.ts +1 -0
- package/src/internal/funding/providers/relay.test.ts +94 -4
- package/src/internal/funding/providers/relay.ts +533 -27
- package/dist/apps/data/routes/funding.d.ts.map +0 -1
- package/dist/apps/data/routes/funding.js +0 -1557
- package/dist/apps/data/routes/funding.js.map +0 -1
- package/src/apps/data/routes/funding.test.ts +0 -2994
- package/src/apps/data/routes/funding.ts +0 -1821
|
@@ -2,6 +2,7 @@ import { Hex } from 'ox'
|
|
|
2
2
|
|
|
3
3
|
import * as Db from '../../db/Db.js'
|
|
4
4
|
import * as FundingDepositAddresses from '../../db/tables/fundingDepositAddresses.js'
|
|
5
|
+
import * as FundingDepositRequestObservations from '../../db/tables/fundingDepositRequestObservations.js'
|
|
5
6
|
import * as FundingDeposits from '../../db/tables/fundingDeposits.js'
|
|
6
7
|
import type * as Metrics from '../../Metrics.js'
|
|
7
8
|
import * as Value from '../Value.js'
|
|
@@ -12,7 +13,14 @@ import * as Subsidy from './Subsidy.js'
|
|
|
12
13
|
const defaultBatchSize = 25
|
|
13
14
|
const defaultLeaseMs = 2 * 60_000
|
|
14
15
|
const defaultPollIntervalMs = 60_000
|
|
16
|
+
const chainProviderFollowUpDelaySeconds = 1
|
|
17
|
+
const chainProviderMaximumFollowUpAttempts = 30
|
|
18
|
+
const followUpDelaySeconds = { delivery: 1, provider: 5, settlement: 2 } as const satisfies Record<
|
|
19
|
+
Provider.Webhook.receive.FollowUp['kind'],
|
|
20
|
+
number
|
|
21
|
+
>
|
|
15
22
|
const maximumPages = 100
|
|
23
|
+
const maximumFollowUpAttempts = 6
|
|
16
24
|
|
|
17
25
|
/** Verified token transfer returned by an authoritative chain reader. */
|
|
18
26
|
export type TransferEvidence = {
|
|
@@ -52,10 +60,11 @@ export function createTracker(options: createTracker.Options): createTracker.Tra
|
|
|
52
60
|
const leaseMs = options.leaseMs ?? defaultLeaseMs
|
|
53
61
|
const pollIntervalMs = options.pollIntervalMs ?? defaultPollIntervalMs
|
|
54
62
|
|
|
55
|
-
const record = (outcome: string, providerId: string) => {
|
|
63
|
+
const record = (outcome: string, providerId: string, trigger: Deposit.DetectionTrigger) => {
|
|
56
64
|
options.metrics?.count('funding_deposit_reconciliation_count', 1, {
|
|
57
65
|
outcome,
|
|
58
66
|
provider: providerId,
|
|
67
|
+
trigger,
|
|
59
68
|
})
|
|
60
69
|
}
|
|
61
70
|
|
|
@@ -63,6 +72,12 @@ export function createTracker(options: createTracker.Options): createTracker.Tra
|
|
|
63
72
|
async reconcile(message) {
|
|
64
73
|
const database = Db.get(options.db)
|
|
65
74
|
const startedAt = now()
|
|
75
|
+
// Legacy poll messages carry a lease version; legacy webhook messages do not.
|
|
76
|
+
const trigger = (() => {
|
|
77
|
+
if (message.trigger) return message.trigger
|
|
78
|
+
if (message.pollLeaseVersion !== undefined) return 'poll'
|
|
79
|
+
return 'webhook'
|
|
80
|
+
})()
|
|
66
81
|
const claimed = await claim(database, message, {
|
|
67
82
|
leaseUntil: new Date(startedAt.getTime() + leaseMs).toISOString(),
|
|
68
83
|
now: startedAt.toISOString(),
|
|
@@ -72,7 +87,7 @@ export function createTracker(options: createTracker.Options): createTracker.Tra
|
|
|
72
87
|
const provider = options.providers.find((candidate) => candidate.id === claimed.providerId)
|
|
73
88
|
if (!provider || !Provider.canListDepositAddressRequests(provider)) {
|
|
74
89
|
await fail(database, claimed, startedAt, pollIntervalMs)
|
|
75
|
-
record('provider_unavailable', claimed.providerId)
|
|
90
|
+
record('provider_unavailable', claimed.providerId, trigger)
|
|
76
91
|
return { type: 'provider_unavailable' }
|
|
77
92
|
}
|
|
78
93
|
|
|
@@ -84,24 +99,70 @@ export function createTracker(options: createTracker.Options): createTracker.Tra
|
|
|
84
99
|
requestId: message.requestId,
|
|
85
100
|
signal: options.signal,
|
|
86
101
|
})
|
|
102
|
+
if (trigger === 'poll')
|
|
103
|
+
await FundingDepositRequestObservations.observePoll(database, {
|
|
104
|
+
depositAddressId: claimed.id,
|
|
105
|
+
observedAt: now().toISOString(),
|
|
106
|
+
providerRequestIds: requests.requests.map((request) => request.id),
|
|
107
|
+
})
|
|
108
|
+
const webhookUpdateObserved = (() => {
|
|
109
|
+
const timing = message.webhookTiming
|
|
110
|
+
if (!timing) return true
|
|
111
|
+
if (!message.requestId) return false
|
|
112
|
+
// Relay can send a webhook before its requests API indexes the same update.
|
|
113
|
+
return requests.requests.some(
|
|
114
|
+
(request) =>
|
|
115
|
+
request.id === message.requestId && request.updatedAt >= timing.providerUpdatedAt,
|
|
116
|
+
)
|
|
117
|
+
})()
|
|
87
118
|
heartbeat.assert()
|
|
88
119
|
let created = 0
|
|
89
120
|
let updated = 0
|
|
90
|
-
const pendingRequestIds = new Set(
|
|
121
|
+
const pendingRequestIds = new Set([
|
|
122
|
+
...requests.pendingRequestIds,
|
|
123
|
+
...(message.requestId ? [message.requestId] : []),
|
|
124
|
+
])
|
|
125
|
+
let followUpKind: Provider.Webhook.receive.FollowUp['kind'] | undefined
|
|
91
126
|
for (const request of requests.requests) {
|
|
92
127
|
const result = await reconcileRequest(database, {
|
|
93
128
|
address: claimed,
|
|
94
129
|
metrics: options.metrics,
|
|
130
|
+
provider,
|
|
95
131
|
request,
|
|
132
|
+
signal: options.signal,
|
|
96
133
|
subsidies: options.subsidies,
|
|
134
|
+
trigger,
|
|
97
135
|
verifyTransfers: options.verifyTransfers,
|
|
98
136
|
})
|
|
99
137
|
heartbeat.assert()
|
|
100
138
|
created += result.created
|
|
101
139
|
updated += result.updated
|
|
102
140
|
if (result.terminal) pendingRequestIds.delete(request.id)
|
|
103
|
-
else
|
|
141
|
+
else {
|
|
142
|
+
pendingRequestIds.add(request.id)
|
|
143
|
+
followUpKind = result.followUp ?? followUpKind ?? 'provider'
|
|
144
|
+
}
|
|
104
145
|
}
|
|
146
|
+
const unattributed = await reconcileUnattributed(database, {
|
|
147
|
+
address: claimed,
|
|
148
|
+
metrics: options.metrics,
|
|
149
|
+
provider,
|
|
150
|
+
signal: options.signal,
|
|
151
|
+
subsidies: options.subsidies,
|
|
152
|
+
verifyTransfers: options.verifyTransfers,
|
|
153
|
+
})
|
|
154
|
+
heartbeat.assert()
|
|
155
|
+
updated += unattributed.updatedIds.size
|
|
156
|
+
if (!unattributed.terminal)
|
|
157
|
+
followUpKind = unattributed.followUp ?? followUpKind ?? 'provider'
|
|
158
|
+
const awaitsProvider = await FundingDeposits.hasUnattributed(database, claimed.id)
|
|
159
|
+
if (message.requestId && message.webhookTiming && !webhookUpdateObserved)
|
|
160
|
+
pendingRequestIds.add(message.requestId)
|
|
161
|
+
if (created > 0)
|
|
162
|
+
options.metrics?.count('funding_deposit_detection_count', created, {
|
|
163
|
+
provider: claimed.providerId,
|
|
164
|
+
trigger,
|
|
165
|
+
})
|
|
105
166
|
|
|
106
167
|
const leaseFailure = await heartbeat.stop()
|
|
107
168
|
if (leaseFailure !== undefined) throw leaseFailure
|
|
@@ -112,22 +173,65 @@ export function createTracker(options: createTracker.Options): createTracker.Tra
|
|
|
112
173
|
nextPollAt: new Date(completedAt.getTime() + pollIntervalMs).toISOString(),
|
|
113
174
|
pollLeaseVersion: claimed.pollLeaseVersion,
|
|
114
175
|
providerRequestIds: claimed.providerRequestIds,
|
|
115
|
-
providerState: reconciliationState(claimed.providerState,
|
|
116
|
-
...pendingRequestIds,
|
|
117
|
-
|
|
176
|
+
providerState: reconciliationState(claimed.providerState, {
|
|
177
|
+
pendingRequestIds: [...pendingRequestIds],
|
|
178
|
+
requests,
|
|
179
|
+
...(message.webhookTiming && message.requestId && webhookUpdateObserved
|
|
180
|
+
? {
|
|
181
|
+
webhookTiming: {
|
|
182
|
+
...message.webhookTiming,
|
|
183
|
+
reconciliationCompletedAt: completedAt.toISOString(),
|
|
184
|
+
requestId: message.requestId,
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
: {}),
|
|
188
|
+
}),
|
|
118
189
|
})
|
|
119
190
|
if (!stored) return { type: 'stale' }
|
|
120
|
-
|
|
191
|
+
if (pendingRequestIds.size > 0 || awaitsProvider) {
|
|
192
|
+
const followUp = nextFollowUp(message, followUpKind ?? 'provider')
|
|
193
|
+
if (followUp)
|
|
194
|
+
await options
|
|
195
|
+
.dispatch(
|
|
196
|
+
{
|
|
197
|
+
addressId: claimed.id,
|
|
198
|
+
followUp: followUp.state,
|
|
199
|
+
...(message.requestId ? { requestId: message.requestId } : {}),
|
|
200
|
+
trigger,
|
|
201
|
+
type: 'funding:deposit-address:reconcile',
|
|
202
|
+
...(message.webhookTiming && !webhookUpdateObserved
|
|
203
|
+
? { webhookTiming: message.webhookTiming }
|
|
204
|
+
: {}),
|
|
205
|
+
},
|
|
206
|
+
{ delaySeconds: followUp.delaySeconds },
|
|
207
|
+
)
|
|
208
|
+
.catch((cause) => {
|
|
209
|
+
record('follow_up_enqueue_failed', claimed.providerId, trigger)
|
|
210
|
+
throw cause
|
|
211
|
+
})
|
|
212
|
+
record(
|
|
213
|
+
followUp ? 'follow_up_enqueued' : 'follow_up_exhausted',
|
|
214
|
+
claimed.providerId,
|
|
215
|
+
trigger,
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
if (message.webhookTiming && webhookUpdateObserved)
|
|
219
|
+
recordWebhookLatency(options.metrics, {
|
|
220
|
+
completedAt,
|
|
221
|
+
providerId: claimed.providerId,
|
|
222
|
+
timing: message.webhookTiming,
|
|
223
|
+
})
|
|
224
|
+
record('completed', claimed.providerId, trigger)
|
|
121
225
|
options.metrics?.histogram(
|
|
122
226
|
'funding_deposit_reconciliation_duration_ms',
|
|
123
227
|
completedAt.getTime() - startedAt.getTime(),
|
|
124
|
-
{ provider: claimed.providerId },
|
|
228
|
+
{ provider: claimed.providerId, trigger },
|
|
125
229
|
)
|
|
126
230
|
return { created, type: 'completed', updated }
|
|
127
231
|
} catch (cause) {
|
|
128
232
|
const leaseFailure = await heartbeat.stop()
|
|
129
233
|
await fail(database, claimed, now(), pollIntervalMs)
|
|
130
|
-
record('failed', claimed.providerId)
|
|
234
|
+
record('failed', claimed.providerId, trigger)
|
|
131
235
|
throw leaseFailure ?? cause
|
|
132
236
|
} finally {
|
|
133
237
|
options.metrics?.flush()
|
|
@@ -159,13 +263,14 @@ export function createTracker(options: createTracker.Options): createTracker.Tra
|
|
|
159
263
|
await options.dispatch({
|
|
160
264
|
addressId: address.id,
|
|
161
265
|
pollLeaseVersion: address.pollLeaseVersion,
|
|
266
|
+
trigger: 'poll',
|
|
162
267
|
type: 'funding:deposit-address:reconcile',
|
|
163
268
|
})
|
|
164
|
-
record('enqueued', provider.id)
|
|
269
|
+
record('enqueued', provider.id, 'poll')
|
|
165
270
|
} catch {
|
|
166
271
|
failed += 1
|
|
167
272
|
await fail(database, address, startedAt, pollIntervalMs)
|
|
168
|
-
record('enqueue_failed', provider.id)
|
|
273
|
+
record('enqueue_failed', provider.id, 'poll')
|
|
169
274
|
}
|
|
170
275
|
}
|
|
171
276
|
if (claimed >= batchSize) break
|
|
@@ -184,7 +289,10 @@ export declare namespace createTracker {
|
|
|
184
289
|
/** Authoritative database or per-invocation database factory. */
|
|
185
290
|
db: Db.Source
|
|
186
291
|
/** Durable reconciliation queue transport. */
|
|
187
|
-
dispatch: (
|
|
292
|
+
dispatch: (
|
|
293
|
+
message: Provider.Webhook.receive.Dispatchable,
|
|
294
|
+
options?: createTracker.DispatchOptions,
|
|
295
|
+
) => Promise<void>
|
|
188
296
|
/** Duration of one reconciliation lease. */
|
|
189
297
|
leaseMs?: number | undefined
|
|
190
298
|
/** Optional bounded operational metrics sink. */
|
|
@@ -203,6 +311,12 @@ export declare namespace createTracker {
|
|
|
203
311
|
verifyTransfers: VerifyTransfers
|
|
204
312
|
}
|
|
205
313
|
|
|
314
|
+
/** Delivery options for one reconciliation queue message. */
|
|
315
|
+
type DispatchOptions = {
|
|
316
|
+
/** Delay before the queue makes the message available. */
|
|
317
|
+
delaySeconds: number
|
|
318
|
+
}
|
|
319
|
+
|
|
206
320
|
/** Result of reconciling one queue message. */
|
|
207
321
|
type ReconcileResult =
|
|
208
322
|
| { type: 'busy' | 'provider_unavailable' | 'stale' }
|
|
@@ -211,7 +325,7 @@ export declare namespace createTracker {
|
|
|
211
325
|
/** Scheduled and queue-driven reusable-address tracker. */
|
|
212
326
|
type Tracker = {
|
|
213
327
|
/** Reconciles one address from provider discovery and verified chain evidence. */
|
|
214
|
-
reconcile(message: Provider.Webhook.receive.
|
|
328
|
+
reconcile(message: Provider.Webhook.receive.Queued): Promise<ReconcileResult>
|
|
215
329
|
/** Claims and enqueues one bounded batch of due reusable addresses. */
|
|
216
330
|
tick(): Promise<{ claimed: number; failed: number }>
|
|
217
331
|
}
|
|
@@ -222,11 +336,7 @@ type ClaimOptions = {
|
|
|
222
336
|
now: string
|
|
223
337
|
}
|
|
224
338
|
|
|
225
|
-
async function claim(
|
|
226
|
-
db: Db.Db,
|
|
227
|
-
message: Provider.Webhook.receive.Dispatchable,
|
|
228
|
-
options: ClaimOptions,
|
|
229
|
-
) {
|
|
339
|
+
async function claim(db: Db.Db, message: Provider.Webhook.receive.Queued, options: ClaimOptions) {
|
|
230
340
|
if (message.pollLeaseVersion === undefined)
|
|
231
341
|
return FundingDepositAddresses.claim(db, {
|
|
232
342
|
id: message.addressId,
|
|
@@ -421,18 +531,42 @@ function readHighWater(state: FundingDepositAddresses.Record['providerState']):
|
|
|
421
531
|
}
|
|
422
532
|
}
|
|
423
533
|
|
|
534
|
+
type ReconciledWebhookTiming = Provider.Webhook.receive.WebhookTiming & {
|
|
535
|
+
/** When Tempo completed the webhook-triggered reconciliation. */
|
|
536
|
+
reconciliationCompletedAt: string
|
|
537
|
+
/** Provider request matched to the observed update. */
|
|
538
|
+
requestId: string
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
type ReconciliationStateOptions = {
|
|
542
|
+
/** Provider requests that still require reconciliation. */
|
|
543
|
+
pendingRequestIds: readonly string[]
|
|
544
|
+
/** Provider requests and high-water state observed in this pass. */
|
|
545
|
+
requests: Requests
|
|
546
|
+
/** Completed webhook timing retained for diagnostics. */
|
|
547
|
+
webhookTiming?: ReconciledWebhookTiming | undefined
|
|
548
|
+
}
|
|
549
|
+
|
|
424
550
|
function reconciliationState(
|
|
425
551
|
state: FundingDepositAddresses.Record['providerState'],
|
|
426
|
-
|
|
427
|
-
pendingRequestIds: readonly string[],
|
|
552
|
+
options: ReconciliationStateOptions,
|
|
428
553
|
) {
|
|
429
554
|
const current = state && typeof state === 'object' ? state : {}
|
|
555
|
+
const reconciliation = (() => {
|
|
556
|
+
const value = current['reconciliation']
|
|
557
|
+
if (value && typeof value === 'object') return value
|
|
558
|
+
return {}
|
|
559
|
+
})()
|
|
430
560
|
return {
|
|
431
561
|
...current,
|
|
432
562
|
reconciliation: {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
563
|
+
...reconciliation,
|
|
564
|
+
pendingRequestIds: options.pendingRequestIds,
|
|
565
|
+
requestIds: options.requests.highWaterRequestIds,
|
|
566
|
+
...(options.requests.highWaterUpdatedAt
|
|
567
|
+
? { updatedAt: options.requests.highWaterUpdatedAt }
|
|
568
|
+
: {}),
|
|
569
|
+
...(options.webhookTiming ? { webhookTiming: options.webhookTiming } : {}),
|
|
436
570
|
},
|
|
437
571
|
}
|
|
438
572
|
}
|
|
@@ -446,49 +580,185 @@ type ObservedSourceTransaction = {
|
|
|
446
580
|
type VerifiedDeposit = {
|
|
447
581
|
deposit: FundingDeposits.Record
|
|
448
582
|
required: bigint
|
|
583
|
+
source: TransferEvidence
|
|
449
584
|
sourceAmount: bigint
|
|
450
585
|
}
|
|
451
586
|
|
|
587
|
+
type ReconcileDeliveryOptions = {
|
|
588
|
+
address: FundingDepositAddresses.Record
|
|
589
|
+
metrics?: Metrics.Metrics | undefined
|
|
590
|
+
provider?: Provider.Provider | undefined
|
|
591
|
+
request?: Provider.listDepositAddressRequests.ReturnType['requests'][number] | undefined
|
|
592
|
+
signal?: AbortSignal | undefined
|
|
593
|
+
subsidies?: Subsidy.Settler | undefined
|
|
594
|
+
verified: readonly VerifiedDeposit[]
|
|
595
|
+
verifyTransfers: VerifyTransfers
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
async function observeProviderDelivery(options: ReconcileDeliveryOptions) {
|
|
599
|
+
const { verified } = options
|
|
600
|
+
const [entry] = verified
|
|
601
|
+
if (
|
|
602
|
+
!entry ||
|
|
603
|
+
verified.length !== 1 ||
|
|
604
|
+
options.address.deliveryStrategy !== 'provider' ||
|
|
605
|
+
options.address.providerOutputToken.tokenKey !==
|
|
606
|
+
options.address.snapshot.destinationToken.tokenKey ||
|
|
607
|
+
(options.request?.status === 'success' &&
|
|
608
|
+
options.request.destinationTransactionHashes.length > 0) ||
|
|
609
|
+
!options.provider ||
|
|
610
|
+
!Provider.canObserveDepositAddressRequest(options.provider)
|
|
611
|
+
)
|
|
612
|
+
return undefined
|
|
613
|
+
|
|
614
|
+
const result = await options.provider
|
|
615
|
+
.observeDepositAddressRequest(
|
|
616
|
+
{
|
|
617
|
+
address: options.address.address,
|
|
618
|
+
destinationChain: options.address.snapshot.destinationChain,
|
|
619
|
+
destinationToken: options.address.providerOutputToken,
|
|
620
|
+
providerState: entry.deposit.providerState ?? undefined,
|
|
621
|
+
recipient: options.address.recipient,
|
|
622
|
+
refundAddress: options.address.refundAddress,
|
|
623
|
+
...(options.request ? { request: options.request } : {}),
|
|
624
|
+
source: entry.source,
|
|
625
|
+
sourceChain: options.address.snapshot.sourceChain,
|
|
626
|
+
sourceToken: options.address.snapshot.sourceToken,
|
|
627
|
+
},
|
|
628
|
+
options.signal ?? new AbortController().signal,
|
|
629
|
+
)
|
|
630
|
+
.catch((cause) => {
|
|
631
|
+
if (options.signal?.aborted) throw cause
|
|
632
|
+
return undefined
|
|
633
|
+
})
|
|
634
|
+
options.metrics?.count('funding_deposit_delivery_observation_count', 1, {
|
|
635
|
+
outcome: result?.type ?? 'failed',
|
|
636
|
+
provider: options.address.providerId,
|
|
637
|
+
})
|
|
638
|
+
return result
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
async function reconcileUnattributed(db: Db.Db, options: reconcileUnattributed.Options) {
|
|
642
|
+
const deposits = await FundingDeposits.listUnattributed(db, options.address.id)
|
|
643
|
+
if (deposits.length === 0) return { terminal: true, updatedIds: new Set<string>() }
|
|
644
|
+
|
|
645
|
+
const transfersByTransaction = new Map(
|
|
646
|
+
await Promise.all(
|
|
647
|
+
unique(deposits.map((deposit) => deposit.sourceTransactionHash)).map(
|
|
648
|
+
async (transactionHash) =>
|
|
649
|
+
[
|
|
650
|
+
transactionHash,
|
|
651
|
+
await options.verifyTransfers({
|
|
652
|
+
chain: options.address.snapshot.sourceChain,
|
|
653
|
+
recipient: options.address.address,
|
|
654
|
+
token: options.address.snapshot.sourceToken,
|
|
655
|
+
transactionHash,
|
|
656
|
+
}),
|
|
657
|
+
] as const,
|
|
658
|
+
),
|
|
659
|
+
),
|
|
660
|
+
)
|
|
661
|
+
let followUp: reconcileRequest.ReturnType['followUp']
|
|
662
|
+
let terminal = true
|
|
663
|
+
const updatedIds = new Set<string>()
|
|
664
|
+
for (const deposit of deposits) {
|
|
665
|
+
const source = transfersByTransaction
|
|
666
|
+
.get(deposit.sourceTransactionHash)
|
|
667
|
+
?.find((transfer) => transfer.transferIndex === deposit.sourceTransferIndex)
|
|
668
|
+
if (!source) {
|
|
669
|
+
terminal = false
|
|
670
|
+
continue
|
|
671
|
+
}
|
|
672
|
+
const delivery = await reconcileDelivery(db, {
|
|
673
|
+
...options,
|
|
674
|
+
verified: [
|
|
675
|
+
{
|
|
676
|
+
deposit,
|
|
677
|
+
required: destinationBaseUnits(
|
|
678
|
+
BigInt(source.amount),
|
|
679
|
+
options.address.snapshot.sourceToken.decimals,
|
|
680
|
+
options.address.snapshot.destinationToken.decimals,
|
|
681
|
+
),
|
|
682
|
+
source,
|
|
683
|
+
sourceAmount: BigInt(source.amount),
|
|
684
|
+
},
|
|
685
|
+
],
|
|
686
|
+
})
|
|
687
|
+
if (delivery.followUp === 'delivery' || !followUp) followUp = delivery.followUp
|
|
688
|
+
if (!delivery.terminal) terminal = false
|
|
689
|
+
for (const id of delivery.updatedIds) updatedIds.add(id)
|
|
690
|
+
}
|
|
691
|
+
return { ...(followUp ? { followUp } : {}), terminal, updatedIds }
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
declare namespace reconcileUnattributed {
|
|
695
|
+
type Options = Omit<ReconcileDeliveryOptions, 'request' | 'verified'>
|
|
696
|
+
}
|
|
697
|
+
|
|
452
698
|
/** Reconciles one provider request against verified chain evidence. */
|
|
453
699
|
export async function reconcileRequest(
|
|
454
700
|
db: Db.Db,
|
|
455
701
|
options: reconcileRequest.Options,
|
|
456
702
|
): Promise<reconcileRequest.ReturnType> {
|
|
703
|
+
const trigger = options.trigger ?? 'poll'
|
|
704
|
+
const observation = await FundingDepositRequestObservations.get(db, {
|
|
705
|
+
depositAddressId: options.address.id,
|
|
706
|
+
providerRequestId: options.request.id,
|
|
707
|
+
})
|
|
708
|
+
if (observation)
|
|
709
|
+
await FundingDeposits.recordRequestObservation(db, {
|
|
710
|
+
depositAddressId: observation.depositAddressId,
|
|
711
|
+
...(observation.pollObservedAt ? { pollObservedAt: observation.pollObservedAt } : {}),
|
|
712
|
+
providerRequestId: observation.providerRequestId,
|
|
713
|
+
...(observation.webhookReceivedAt
|
|
714
|
+
? { webhookReceivedAt: observation.webhookReceivedAt }
|
|
715
|
+
: {}),
|
|
716
|
+
})
|
|
457
717
|
let created = 0
|
|
458
718
|
const observed: ObservedSourceTransaction[] = []
|
|
459
719
|
// Persist provider observations first so transient chain failures do not hide acknowledged deposits.
|
|
460
720
|
for (const transactionHash of unique(options.request.sourceTransactionHashes)) {
|
|
461
|
-
|
|
721
|
+
const deposits = await FundingDeposits.withSourceTransaction(db, {
|
|
462
722
|
depositAddressId: options.address.id,
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
if (deposits.length === 0)
|
|
467
|
-
deposits = await FundingDeposits.listBySourceTransaction(db, {
|
|
468
|
-
depositAddressId: options.address.id,
|
|
469
|
-
sourceTransactionHash: transactionHash,
|
|
470
|
-
})
|
|
471
|
-
if (deposits.length === 0) {
|
|
472
|
-
deposits = [
|
|
473
|
-
await Deposit.create(db, {
|
|
474
|
-
now: new Date(options.request.createdAt),
|
|
723
|
+
fn: async (tx): Promise<FundingDeposits.Record[]> => {
|
|
724
|
+
let records = await FundingDeposits.listByProviderObservation(tx, {
|
|
725
|
+
depositAddressId: options.address.id,
|
|
475
726
|
providerRequestId: options.request.id,
|
|
476
|
-
snapshot: {
|
|
477
|
-
depositAddressId: options.address.id,
|
|
478
|
-
destinationChain: options.address.snapshot.destinationChain,
|
|
479
|
-
destinationToken: options.address.snapshot.destinationToken,
|
|
480
|
-
provider: options.address.snapshot.provider,
|
|
481
|
-
recipient: options.address.recipient,
|
|
482
|
-
refundAddress: options.address.refundAddress,
|
|
483
|
-
sourceChain: options.address.snapshot.sourceChain,
|
|
484
|
-
sourceToken: options.address.snapshot.sourceToken,
|
|
485
|
-
sourceTransactionHashes: [transactionHash],
|
|
486
|
-
},
|
|
487
727
|
sourceTransactionHash: transactionHash,
|
|
488
|
-
})
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
728
|
+
})
|
|
729
|
+
if (records.length === 0)
|
|
730
|
+
records = await FundingDeposits.listBySourceTransaction(tx, {
|
|
731
|
+
depositAddressId: options.address.id,
|
|
732
|
+
sourceTransactionHash: transactionHash,
|
|
733
|
+
})
|
|
734
|
+
if (records.length > 0) return records
|
|
735
|
+
created += 1
|
|
736
|
+
return [
|
|
737
|
+
await Deposit.create(tx, {
|
|
738
|
+
detectionTrigger: trigger,
|
|
739
|
+
now: new Date(options.request.createdAt),
|
|
740
|
+
...(observation?.pollObservedAt ? { pollObservedAt: observation.pollObservedAt } : {}),
|
|
741
|
+
providerRequestId: options.request.id,
|
|
742
|
+
snapshot: {
|
|
743
|
+
depositAddressId: options.address.id,
|
|
744
|
+
destinationChain: options.address.snapshot.destinationChain,
|
|
745
|
+
destinationToken: options.address.snapshot.destinationToken,
|
|
746
|
+
provider: options.address.snapshot.provider,
|
|
747
|
+
recipient: options.address.recipient,
|
|
748
|
+
refundAddress: options.address.refundAddress,
|
|
749
|
+
sourceChain: options.address.snapshot.sourceChain,
|
|
750
|
+
sourceToken: options.address.snapshot.sourceToken,
|
|
751
|
+
sourceTransactionHashes: [transactionHash],
|
|
752
|
+
},
|
|
753
|
+
sourceTransactionHash: transactionHash,
|
|
754
|
+
...(observation?.webhookReceivedAt
|
|
755
|
+
? { webhookReceivedAt: observation.webhookReceivedAt }
|
|
756
|
+
: {}),
|
|
757
|
+
}),
|
|
758
|
+
]
|
|
759
|
+
},
|
|
760
|
+
sourceTransactionHash: transactionHash,
|
|
761
|
+
})
|
|
492
762
|
observed.push({ deposits, transactionHash, transfers: [] })
|
|
493
763
|
}
|
|
494
764
|
|
|
@@ -508,7 +778,6 @@ export async function reconcileRequest(
|
|
|
508
778
|
|
|
509
779
|
const sourceEvidenceComplete =
|
|
510
780
|
sources.length > 0 && sources.every((source) => source.transfers.length > 0)
|
|
511
|
-
let terminal = sourceEvidenceComplete
|
|
512
781
|
const updatedIds = new Set<string>()
|
|
513
782
|
const verified: VerifiedDeposit[] = []
|
|
514
783
|
for (const source of sources) {
|
|
@@ -539,7 +808,9 @@ export async function reconcileRequest(
|
|
|
539
808
|
)
|
|
540
809
|
if (!deposit) {
|
|
541
810
|
deposit = await Deposit.create(db, {
|
|
811
|
+
detectionTrigger: trigger,
|
|
542
812
|
now: new Date(options.request.createdAt),
|
|
813
|
+
...(observation?.pollObservedAt ? { pollObservedAt: observation.pollObservedAt } : {}),
|
|
543
814
|
providerRequestId: options.request.id,
|
|
544
815
|
providerTransferIndex,
|
|
545
816
|
snapshot: {
|
|
@@ -558,15 +829,24 @@ export async function reconcileRequest(
|
|
|
558
829
|
},
|
|
559
830
|
sourceTransactionHash: transfer.transactionHash,
|
|
560
831
|
sourceTransferIndex: transfer.transferIndex,
|
|
832
|
+
...(observation?.webhookReceivedAt
|
|
833
|
+
? { webhookReceivedAt: observation.webhookReceivedAt }
|
|
834
|
+
: {}),
|
|
561
835
|
})
|
|
562
836
|
source.deposits.push(deposit)
|
|
563
837
|
created += 1
|
|
564
838
|
}
|
|
565
|
-
if (
|
|
839
|
+
if (
|
|
840
|
+
!Deposit.isTerminal(deposit.status) ||
|
|
841
|
+
deposit.providerRequestId === null ||
|
|
842
|
+
!deposit.providerRequestIds.includes(options.request.id)
|
|
843
|
+
) {
|
|
566
844
|
const result = await Deposit.transition(db, {
|
|
567
845
|
expectedVersion: deposit.version,
|
|
568
846
|
id: deposit.id,
|
|
847
|
+
providerRequestId: options.request.id,
|
|
569
848
|
providerRequestIds: unique([...deposit.providerRequestIds, options.request.id]),
|
|
849
|
+
providerTransferIndex,
|
|
570
850
|
snapshot: {
|
|
571
851
|
...deposit.snapshot,
|
|
572
852
|
...(options.address.snapshot.subsidize ? { destinationAmountRequired } : {}),
|
|
@@ -581,14 +861,72 @@ export async function reconcileRequest(
|
|
|
581
861
|
updatedIds.add(deposit.id)
|
|
582
862
|
}
|
|
583
863
|
}
|
|
584
|
-
verified.push({ deposit, required, sourceAmount: BigInt(transfer.amount) })
|
|
864
|
+
verified.push({ deposit, required, source: transfer, sourceAmount: BigInt(transfer.amount) })
|
|
585
865
|
}
|
|
586
866
|
}
|
|
587
867
|
if (verified.length === 0) return { created, terminal: false, updated: updatedIds.size }
|
|
588
868
|
if (!sourceEvidenceComplete) return { created, terminal: false, updated: updatedIds.size }
|
|
869
|
+
const delivery = await reconcileDelivery(db, { ...options, verified })
|
|
870
|
+
for (const id of delivery.updatedIds) updatedIds.add(id)
|
|
871
|
+
return {
|
|
872
|
+
created,
|
|
873
|
+
...(delivery.followUp ? { followUp: delivery.followUp } : {}),
|
|
874
|
+
terminal: delivery.terminal,
|
|
875
|
+
updated: updatedIds.size,
|
|
876
|
+
}
|
|
877
|
+
}
|
|
589
878
|
|
|
879
|
+
export declare namespace reconcileRequest {
|
|
880
|
+
/** Provider request, address, and settlement dependencies. */
|
|
881
|
+
type Options = {
|
|
882
|
+
/** Reusable deposit address attributed by the provider. */
|
|
883
|
+
address: FundingDepositAddresses.Record
|
|
884
|
+
/** Optional bounded operational metrics sink. */
|
|
885
|
+
metrics?: Metrics.Metrics | undefined
|
|
886
|
+
/** Provider integration that discovered the request. */
|
|
887
|
+
provider?: Provider.Provider | undefined
|
|
888
|
+
/** Provider request containing discovered chain transactions. */
|
|
889
|
+
request: Provider.listDepositAddressRequests.ReturnType['requests'][number]
|
|
890
|
+
/** Abort signal forwarded to provider delivery observation. */
|
|
891
|
+
signal?: AbortSignal | undefined
|
|
892
|
+
/** Optional pathUSD-funded subsidy settler. */
|
|
893
|
+
subsidies?: Subsidy.Settler | undefined
|
|
894
|
+
/** Mechanism that requested reconciliation. */
|
|
895
|
+
trigger?: Deposit.DetectionTrigger | undefined
|
|
896
|
+
/** Authoritative source and destination transfer verifier. */
|
|
897
|
+
verifyTransfers: VerifyTransfers
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/** Material changes produced by one provider request. */
|
|
901
|
+
type ReturnType = {
|
|
902
|
+
/** Number of newly detected source transfers. */
|
|
903
|
+
created: number
|
|
904
|
+
/** Work that needs a bounded delayed follow-up. */
|
|
905
|
+
followUp?: 'delivery' | 'settlement' | undefined
|
|
906
|
+
/** Whether every deposit from this request reached a terminal state. */
|
|
907
|
+
terminal: boolean
|
|
908
|
+
/** Number of existing or new deposits materially changed. */
|
|
909
|
+
updated: number
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
async function reconcileDelivery(db: Db.Db, options: ReconcileDeliveryOptions) {
|
|
914
|
+
let followUp: reconcileRequest.ReturnType['followUp']
|
|
915
|
+
let terminal = true
|
|
916
|
+
const updatedIds = new Set<string>()
|
|
917
|
+
if (options.verified.every((entry) => Deposit.isTerminal(entry.deposit.status)))
|
|
918
|
+
return { terminal, updatedIds }
|
|
919
|
+
const deliveryObservation = await observeProviderDelivery(options)
|
|
920
|
+
if (deliveryObservation?.type === 'pending') followUp = 'delivery'
|
|
921
|
+
const providerTransactionHashes = options.request?.destinationTransactionHashes ?? []
|
|
922
|
+
const destinationTransactionHashes = unique([
|
|
923
|
+
...providerTransactionHashes,
|
|
924
|
+
...(deliveryObservation?.type === 'verified'
|
|
925
|
+
? deliveryObservation.destinationTransactionHashes
|
|
926
|
+
: []),
|
|
927
|
+
])
|
|
590
928
|
const destinationEvidence = await Promise.all(
|
|
591
|
-
|
|
929
|
+
destinationTransactionHashes.map((transactionHash) =>
|
|
592
930
|
options.verifyTransfers({
|
|
593
931
|
chain: options.address.snapshot.destinationChain,
|
|
594
932
|
recipient: options.address.recipient,
|
|
@@ -599,17 +937,18 @@ export async function reconcileRequest(
|
|
|
599
937
|
)
|
|
600
938
|
const destinationTransfers = destinationEvidence.flat()
|
|
601
939
|
const providerEvidenceComplete =
|
|
602
|
-
options.request
|
|
940
|
+
(options.request?.status === 'success' || deliveryObservation?.type === 'verified') &&
|
|
941
|
+
destinationEvidence.length > 0 &&
|
|
603
942
|
destinationEvidence.every((transfers) => transfers.length > 0)
|
|
604
943
|
const providerOutput = destinationTransfers.reduce(
|
|
605
944
|
(total, transfer) => total + BigInt(transfer.amount),
|
|
606
945
|
0n,
|
|
607
946
|
)
|
|
608
|
-
const totalRequired = verified.reduce((total, entry) => total + entry.required, 0n)
|
|
947
|
+
const totalRequired = options.verified.reduce((total, entry) => total + entry.required, 0n)
|
|
609
948
|
let allocated = 0n
|
|
610
949
|
let cumulativeRequired = 0n
|
|
611
950
|
|
|
612
|
-
for (const entry of verified) {
|
|
951
|
+
for (const entry of options.verified) {
|
|
613
952
|
const { required, sourceAmount } = entry
|
|
614
953
|
cumulativeRequired += required
|
|
615
954
|
const nextAllocated =
|
|
@@ -635,6 +974,7 @@ export async function reconcileRequest(
|
|
|
635
974
|
provider: options.address.providerId,
|
|
636
975
|
})
|
|
637
976
|
if (settlement.status === 'bridging' || settlement.status === 'settling') terminal = false
|
|
977
|
+
if (settlement.status === 'settling') followUp = 'settlement'
|
|
638
978
|
const destinationAmount = Value.tokenAmount({
|
|
639
979
|
baseUnits: delivered + settlement.delivered,
|
|
640
980
|
currency: options.address.snapshot.destinationToken.currency,
|
|
@@ -652,10 +992,21 @@ export async function reconcileRequest(
|
|
|
652
992
|
}),
|
|
653
993
|
}
|
|
654
994
|
: {}),
|
|
655
|
-
providerRequestIds:
|
|
995
|
+
providerRequestIds: options.request
|
|
996
|
+
? unique([...deposit.providerRequestIds, options.request.id])
|
|
997
|
+
: deposit.providerRequestIds,
|
|
998
|
+
...(deliveryObservation?.providerState
|
|
999
|
+
? {
|
|
1000
|
+
providerState: {
|
|
1001
|
+
...deposit.providerState,
|
|
1002
|
+
...deliveryObservation.providerState,
|
|
1003
|
+
},
|
|
1004
|
+
}
|
|
1005
|
+
: {}),
|
|
656
1006
|
providerTransactionHashes: unique([
|
|
657
1007
|
...deposit.providerTransactionHashes,
|
|
658
|
-
...
|
|
1008
|
+
...providerTransactionHashes,
|
|
1009
|
+
...(deliveryObservation?.providerTransactionHashes ?? []),
|
|
659
1010
|
]),
|
|
660
1011
|
...(settlement.settlementTransaction
|
|
661
1012
|
? { settlementTransaction: settlement.settlementTransaction }
|
|
@@ -697,33 +1048,7 @@ export async function reconcileRequest(
|
|
|
697
1048
|
}
|
|
698
1049
|
}
|
|
699
1050
|
}
|
|
700
|
-
return {
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
export declare namespace reconcileRequest {
|
|
704
|
-
/** Provider request, address, and settlement dependencies. */
|
|
705
|
-
type Options = {
|
|
706
|
-
/** Reusable deposit address attributed by the provider. */
|
|
707
|
-
address: FundingDepositAddresses.Record
|
|
708
|
-
/** Optional bounded operational metrics sink. */
|
|
709
|
-
metrics?: Metrics.Metrics | undefined
|
|
710
|
-
/** Provider request containing discovered chain transactions. */
|
|
711
|
-
request: Provider.listDepositAddressRequests.ReturnType['requests'][number]
|
|
712
|
-
/** Optional pathUSD-funded subsidy settler. */
|
|
713
|
-
subsidies?: Subsidy.Settler | undefined
|
|
714
|
-
/** Authoritative source and destination transfer verifier. */
|
|
715
|
-
verifyTransfers: VerifyTransfers
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
/** Material changes produced by one provider request. */
|
|
719
|
-
type ReturnType = {
|
|
720
|
-
/** Number of newly detected source transfers. */
|
|
721
|
-
created: number
|
|
722
|
-
/** Whether every deposit from this request reached a terminal state. */
|
|
723
|
-
terminal: boolean
|
|
724
|
-
/** Number of existing or new deposits materially changed. */
|
|
725
|
-
updated: number
|
|
726
|
-
}
|
|
1051
|
+
return { ...(followUp ? { followUp } : {}), terminal, updatedIds }
|
|
727
1052
|
}
|
|
728
1053
|
|
|
729
1054
|
type ReconcileSubsidyOptions = {
|
|
@@ -919,6 +1244,60 @@ function unique(values: readonly string[]) {
|
|
|
919
1244
|
return [...new Set(values)]
|
|
920
1245
|
}
|
|
921
1246
|
|
|
1247
|
+
function nextFollowUp(
|
|
1248
|
+
message: Provider.Webhook.receive.Queued,
|
|
1249
|
+
kind: Provider.Webhook.receive.FollowUp['kind'],
|
|
1250
|
+
) {
|
|
1251
|
+
const attempt = message.followUp?.kind === kind ? message.followUp.attempt + 1 : 0
|
|
1252
|
+
const chainProvider = kind === 'provider' && message.trigger === 'chain'
|
|
1253
|
+
const maximumAttempts = chainProvider
|
|
1254
|
+
? chainProviderMaximumFollowUpAttempts
|
|
1255
|
+
: maximumFollowUpAttempts
|
|
1256
|
+
if (attempt >= maximumAttempts) return undefined
|
|
1257
|
+
return {
|
|
1258
|
+
delaySeconds: chainProvider ? chainProviderFollowUpDelaySeconds : followUpDelaySeconds[kind],
|
|
1259
|
+
state: { attempt, kind },
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
type RecordWebhookLatencyOptions = {
|
|
1264
|
+
/** When Tempo completed the webhook-triggered reconciliation. */
|
|
1265
|
+
completedAt: Date
|
|
1266
|
+
/** Funding provider that sent the webhook. */
|
|
1267
|
+
providerId: string
|
|
1268
|
+
/** Authenticated timestamps carried from webhook receipt. */
|
|
1269
|
+
timing: Provider.Webhook.receive.WebhookTiming
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
function recordWebhookLatency(
|
|
1273
|
+
metrics: Metrics.Metrics | undefined,
|
|
1274
|
+
options: RecordWebhookLatencyOptions,
|
|
1275
|
+
) {
|
|
1276
|
+
if (!metrics) return
|
|
1277
|
+
const completedAt = options.completedAt.getTime()
|
|
1278
|
+
const enqueuedAt = Date.parse(options.timing.enqueuedAt)
|
|
1279
|
+
const providerUpdatedAt = Date.parse(options.timing.providerUpdatedAt)
|
|
1280
|
+
const receivedAt = Date.parse(options.timing.receivedAt)
|
|
1281
|
+
const sentAt = Date.parse(options.timing.sentAt)
|
|
1282
|
+
const segments = [
|
|
1283
|
+
{ from: providerUpdatedAt, segment: 'provider_to_sent', to: sentAt },
|
|
1284
|
+
{ from: sentAt, segment: 'sent_to_received', to: receivedAt },
|
|
1285
|
+
{ from: receivedAt, segment: 'received_to_enqueued', to: enqueuedAt },
|
|
1286
|
+
{ from: enqueuedAt, segment: 'enqueued_to_reconciled', to: completedAt },
|
|
1287
|
+
{ from: providerUpdatedAt, segment: 'provider_to_reconciled', to: completedAt },
|
|
1288
|
+
] as const
|
|
1289
|
+
for (const segment of segments)
|
|
1290
|
+
metrics.histogram(
|
|
1291
|
+
'funding_deposit_webhook_latency_ms',
|
|
1292
|
+
Math.max(0, segment.to - segment.from),
|
|
1293
|
+
{
|
|
1294
|
+
provider: options.providerId,
|
|
1295
|
+
segment: segment.segment,
|
|
1296
|
+
status: options.timing.providerStatus,
|
|
1297
|
+
},
|
|
1298
|
+
)
|
|
1299
|
+
}
|
|
1300
|
+
|
|
922
1301
|
/** Provider pagination exceeded the bounded reconciliation limit. */
|
|
923
1302
|
export class PaginationLimitError extends Error {
|
|
924
1303
|
override name = 'Funding.Reconciliation.PaginationLimitError'
|