tapimo 0.7.2 → 0.8.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 +20434 -19484
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +62 -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 +168 -168
- 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 +10305 -12785
- 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 +24 -24
- package/dist/apps/data/routes/addresses.d.ts +288 -288
- package/dist/apps/data/routes/balances.d.ts +6 -6
- package/dist/apps/data/routes/blocks.d.ts +39 -39
- package/dist/apps/data/routes/coingecko.d.ts +170 -170
- package/dist/apps/data/routes/earn.d.ts +436 -6
- package/dist/apps/data/routes/earn.d.ts.map +1 -1
- package/dist/apps/data/routes/earn.js +631 -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 +1 -1
- 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 +6 -6
- package/dist/apps/data/routes/transactions.d.ts +2 -2
- package/dist/apps/data/routes/transfers.d.ts +1 -1
- 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} +1717 -3035
- package/dist/apps/funding/App.d.ts.map +1 -0
- package/dist/apps/funding/App.js +46 -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 +2388 -0
- package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -0
- package/dist/apps/funding/routes/deposit-addresses.js +860 -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 +48 -48
- 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 +37 -37
- 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 +18 -18
- 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 +2 -2
- 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 +37 -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 +57 -1
- package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
- package/dist/db/tables/fundingDeposits.js +82 -0
- package/dist/db/tables/fundingDeposits.js.map +1 -1
- package/dist/internal/Auth.d.ts +11 -11
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +41 -15
- 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 +257 -48
- 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 +434 -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 +23 -5
- package/src/App.ts +76 -5
- package/src/Client.test-d.ts +171 -19
- 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 +100 -2
- package/src/apps/data/App.test.ts +0 -12
- package/src/apps/data/App.ts +4 -8
- package/src/apps/data/routes/earn.test.ts +283 -0
- package/src/apps/data/routes/earn.ts +800 -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 +63 -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 +1628 -0
- package/src/apps/funding/routes/deposit-addresses.ts +1015 -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 +1255 -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 +62 -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 +138 -0
- package/src/db/tables/fundingDeposits.ts +145 -1
- package/src/internal/Auth.test.ts +177 -5
- package/src/internal/Auth.ts +48 -18
- 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 +1052 -35
- package/src/internal/funding/Reconciliation.ts +328 -56
- 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 +528 -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
package/src/cloudflare.ts
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
// @ts-ignore
|
|
5
5
|
import { DurableObject as CloudflareDurableObject } from 'cloudflare:workers'
|
|
6
6
|
|
|
7
|
+
import * as Db from './db/Db.js'
|
|
8
|
+
import * as Funding from './internal/funding/index.js'
|
|
9
|
+
import * as Metrics from './Metrics.js'
|
|
7
10
|
import * as Store from './internal/Store.js'
|
|
8
11
|
|
|
9
12
|
type State = ConstructorParameters<typeof CloudflareDurableObject>[0]
|
|
@@ -68,3 +71,279 @@ export class DurableObject extends CloudflareDurableObject {
|
|
|
68
71
|
return this.#store.swap!(key, expected, next, options)
|
|
69
72
|
}
|
|
70
73
|
}
|
|
74
|
+
|
|
75
|
+
const fundingSourceAlarmIntervalMs = 500
|
|
76
|
+
const fundingSourceRetryIntervalMs = 2_000
|
|
77
|
+
|
|
78
|
+
type FundingSourceConfiguration = {
|
|
79
|
+
chainId: string
|
|
80
|
+
rpcUrls: readonly string[]
|
|
81
|
+
tokenAddress: string
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
type FundingSourceRoute = Funding.SourceObservation.Route
|
|
85
|
+
|
|
86
|
+
type FundingSourceRegisterOptions = FundingSourceConfiguration & {
|
|
87
|
+
route: FundingSourceRoute
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
type FundingSourceSyncOptions = FundingSourceConfiguration & {
|
|
91
|
+
routes: readonly FundingSourceRoute[]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
type FundingSourceConfigurationRow = {
|
|
95
|
+
value: string
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
type FundingSourceRouteRow = {
|
|
99
|
+
address: string
|
|
100
|
+
id: string
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
type FundingSourceStateRow = {
|
|
104
|
+
value: string
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Cloudflare Durable Object that detects EVM funding deposits before provider indexing. */
|
|
108
|
+
export class FundingSourceObserver<
|
|
109
|
+
environment extends FundingSourceObserver.Environment = FundingSourceObserver.Environment,
|
|
110
|
+
> extends CloudflareDurableObject {
|
|
111
|
+
/** Durable Object storage and alarm context. */
|
|
112
|
+
protected context: State
|
|
113
|
+
/** Cloudflare bindings used by the observer. */
|
|
114
|
+
protected environment: environment
|
|
115
|
+
|
|
116
|
+
constructor(ctx: State, env: environment) {
|
|
117
|
+
super(ctx, env as never)
|
|
118
|
+
this.context = ctx
|
|
119
|
+
this.environment = env
|
|
120
|
+
void ctx.blockConcurrencyWhile(async () => {
|
|
121
|
+
ctx.storage.sql.exec(
|
|
122
|
+
'CREATE TABLE IF NOT EXISTS configuration (id INTEGER PRIMARY KEY CHECK (id = 1), value TEXT NOT NULL)',
|
|
123
|
+
)
|
|
124
|
+
ctx.storage.sql.exec('CREATE TABLE IF NOT EXISTS backfills (id TEXT PRIMARY KEY)')
|
|
125
|
+
ctx.storage.sql.exec(
|
|
126
|
+
'CREATE TABLE IF NOT EXISTS routes (id TEXT PRIMARY KEY, address TEXT NOT NULL UNIQUE)',
|
|
127
|
+
)
|
|
128
|
+
ctx.storage.sql.exec(
|
|
129
|
+
'CREATE TABLE IF NOT EXISTS state (key TEXT PRIMARY KEY, value TEXT NOT NULL)',
|
|
130
|
+
)
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Registers one newly created deposit address and starts source observation. */
|
|
135
|
+
async register(options: FundingSourceRegisterOptions): Promise<void> {
|
|
136
|
+
const configuration = serializeFundingSourceConfiguration(options)
|
|
137
|
+
const route = this.context.storage.sql
|
|
138
|
+
.exec<FundingSourceRouteRow>('SELECT id, address FROM routes WHERE id = ?', options.route.id)
|
|
139
|
+
.toArray()[0]
|
|
140
|
+
this.context.storage.transactionSync(() => {
|
|
141
|
+
this.context.storage.sql.exec(
|
|
142
|
+
'INSERT OR REPLACE INTO configuration (id, value) VALUES (1, ?)',
|
|
143
|
+
configuration,
|
|
144
|
+
)
|
|
145
|
+
this.context.storage.sql.exec(
|
|
146
|
+
'INSERT OR REPLACE INTO routes (id, address) VALUES (?, ?)',
|
|
147
|
+
options.route.id,
|
|
148
|
+
options.route.address.toLowerCase(),
|
|
149
|
+
)
|
|
150
|
+
if (!route || route.address.toLowerCase() !== options.route.address.toLowerCase())
|
|
151
|
+
this.context.storage.sql.exec(
|
|
152
|
+
'INSERT OR IGNORE INTO backfills (id) VALUES (?)',
|
|
153
|
+
options.route.id,
|
|
154
|
+
)
|
|
155
|
+
})
|
|
156
|
+
await this.#ensureAlarm()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Replaces the active routes from the database and repairs alarm scheduling. */
|
|
160
|
+
async sync(options: FundingSourceSyncOptions): Promise<void> {
|
|
161
|
+
const configuration = serializeFundingSourceConfiguration(options)
|
|
162
|
+
const existing = new Map(
|
|
163
|
+
this.context.storage.sql
|
|
164
|
+
.exec<FundingSourceRouteRow>('SELECT id, address FROM routes ORDER BY id')
|
|
165
|
+
.toArray()
|
|
166
|
+
.map((route: FundingSourceRouteRow) => [route.id, route.address.toLowerCase()] as const),
|
|
167
|
+
)
|
|
168
|
+
const changed = options.routes.filter(
|
|
169
|
+
(route) => existing.get(route.id) !== route.address.toLowerCase(),
|
|
170
|
+
)
|
|
171
|
+
this.context.storage.transactionSync(() => {
|
|
172
|
+
this.context.storage.sql.exec(
|
|
173
|
+
'INSERT OR REPLACE INTO configuration (id, value) VALUES (1, ?)',
|
|
174
|
+
configuration,
|
|
175
|
+
)
|
|
176
|
+
this.context.storage.sql.exec('DELETE FROM routes')
|
|
177
|
+
for (const route of options.routes)
|
|
178
|
+
this.context.storage.sql.exec(
|
|
179
|
+
'INSERT INTO routes (id, address) VALUES (?, ?)',
|
|
180
|
+
route.id,
|
|
181
|
+
route.address.toLowerCase(),
|
|
182
|
+
)
|
|
183
|
+
this.context.storage.sql.exec('DELETE FROM backfills WHERE id NOT IN (SELECT id FROM routes)')
|
|
184
|
+
for (const route of changed)
|
|
185
|
+
this.context.storage.sql.exec('INSERT OR IGNORE INTO backfills (id) VALUES (?)', route.id)
|
|
186
|
+
})
|
|
187
|
+
if (options.routes.length === 0) {
|
|
188
|
+
await this.context.storage.deleteAlarm()
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
await this.#ensureAlarm()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Scans confirmed source blocks and persists newly observed deposits. */
|
|
195
|
+
async alarm(): Promise<void> {
|
|
196
|
+
const metrics = this.createMetrics()
|
|
197
|
+
const configuration = this.#readConfiguration()
|
|
198
|
+
const routes: FundingSourceRouteRow[] = this.context.storage.sql
|
|
199
|
+
.exec<FundingSourceRouteRow>('SELECT id, address FROM routes ORDER BY id')
|
|
200
|
+
.toArray()
|
|
201
|
+
if (!configuration || routes.length === 0) return
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const cursor = this.context.storage.sql
|
|
205
|
+
.exec<FundingSourceStateRow>("SELECT value FROM state WHERE key = 'cursor'")
|
|
206
|
+
.toArray()[0]
|
|
207
|
+
const result = await Funding.SourceObservation.scan({
|
|
208
|
+
chainId: configuration.chainId,
|
|
209
|
+
...(cursor ? { cursor: BigInt(cursor.value) } : {}),
|
|
210
|
+
routes,
|
|
211
|
+
rpcUrls: configuration.rpcUrls,
|
|
212
|
+
tokenAddress: configuration.tokenAddress,
|
|
213
|
+
})
|
|
214
|
+
const backfills = this.context.storage.sql
|
|
215
|
+
.exec<FundingSourceRouteRow>(
|
|
216
|
+
'SELECT routes.id, routes.address FROM routes INNER JOIN backfills ON backfills.id = routes.id ORDER BY routes.id',
|
|
217
|
+
)
|
|
218
|
+
.toArray()
|
|
219
|
+
// Backfill new routes independently so the main cursor retains any older shard backlog.
|
|
220
|
+
const backfill = await (async () => {
|
|
221
|
+
if (!cursor || backfills.length === 0) return undefined
|
|
222
|
+
return Funding.SourceObservation.scan({
|
|
223
|
+
chainId: configuration.chainId,
|
|
224
|
+
routes: backfills,
|
|
225
|
+
rpcUrls: configuration.rpcUrls,
|
|
226
|
+
tokenAddress: configuration.tokenAddress,
|
|
227
|
+
})
|
|
228
|
+
})()
|
|
229
|
+
const database = Db.get(() =>
|
|
230
|
+
Db.postgres({ connectionString: this.environment.HYPERDRIVE.connectionString }),
|
|
231
|
+
)
|
|
232
|
+
const routesById = new Map(routes.map((route) => [route.id, route] as const))
|
|
233
|
+
const observations = new Map(
|
|
234
|
+
[...result.observations, ...(backfill?.observations ?? [])].map((observation) => [
|
|
235
|
+
`${observation.addressId}:${observation.transactionHash.toLowerCase()}:${observation.transferIndex}`,
|
|
236
|
+
observation,
|
|
237
|
+
]),
|
|
238
|
+
).values()
|
|
239
|
+
for (const observation of observations) {
|
|
240
|
+
const route = routesById.get(observation.addressId)
|
|
241
|
+
if (!route) continue
|
|
242
|
+
const persisted = await Funding.SourceObservation.observe(database, {
|
|
243
|
+
...observation,
|
|
244
|
+
recipient: route.address,
|
|
245
|
+
})
|
|
246
|
+
if (
|
|
247
|
+
persisted.type === 'ignored' ||
|
|
248
|
+
persisted.record.providerRequestId !== null ||
|
|
249
|
+
Funding.Deposit.isTerminal(persisted.record.status)
|
|
250
|
+
)
|
|
251
|
+
continue
|
|
252
|
+
await this.environment.FUNDING_RECONCILIATION_QUEUE.send({
|
|
253
|
+
addressId: observation.addressId,
|
|
254
|
+
trigger: 'chain',
|
|
255
|
+
type: 'funding:deposit-address:reconcile',
|
|
256
|
+
})
|
|
257
|
+
metrics.count('funding_source_observer_deposit_count', 1, {
|
|
258
|
+
chain: configuration.chainId,
|
|
259
|
+
outcome: persisted.type,
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
this.context.storage.transactionSync(() => {
|
|
263
|
+
this.context.storage.sql.exec(
|
|
264
|
+
"INSERT OR REPLACE INTO state (key, value) VALUES ('cursor', ?)",
|
|
265
|
+
result.cursor.toString(),
|
|
266
|
+
)
|
|
267
|
+
for (const route of backfills)
|
|
268
|
+
this.context.storage.sql.exec('DELETE FROM backfills WHERE id = ?', route.id)
|
|
269
|
+
})
|
|
270
|
+
metrics.count('funding_source_observer_scan_count', 1, {
|
|
271
|
+
chain: configuration.chainId,
|
|
272
|
+
outcome: 'completed',
|
|
273
|
+
})
|
|
274
|
+
metrics.gauge(
|
|
275
|
+
'funding_source_observer_block_lag',
|
|
276
|
+
Math.max(0, Number(result.head - result.cursor)),
|
|
277
|
+
{ chain: configuration.chainId },
|
|
278
|
+
)
|
|
279
|
+
await this.context.storage.setAlarm(
|
|
280
|
+
result.cursor < result.head ? Date.now() : Date.now() + fundingSourceAlarmIntervalMs,
|
|
281
|
+
)
|
|
282
|
+
} catch (cause) {
|
|
283
|
+
metrics.count('funding_source_observer_scan_count', 1, {
|
|
284
|
+
chain: configuration.chainId,
|
|
285
|
+
outcome: 'failed',
|
|
286
|
+
})
|
|
287
|
+
this.capture(cause)
|
|
288
|
+
await this.context.storage.setAlarm(Date.now() + fundingSourceRetryIntervalMs)
|
|
289
|
+
} finally {
|
|
290
|
+
metrics.flush()
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Reports one source-observer failure. */
|
|
295
|
+
protected capture(cause: unknown): void {
|
|
296
|
+
console.error(cause)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Creates the operational metric sink used by one alarm invocation. */
|
|
300
|
+
protected createMetrics(): Metrics.Metrics {
|
|
301
|
+
return Metrics.cloudflare({
|
|
302
|
+
enabled: Boolean(this.environment.ENVIRONMENT),
|
|
303
|
+
environment: this.environment.ENVIRONMENT ?? 'development',
|
|
304
|
+
service: 'tapimo',
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
#configuration(): string | undefined {
|
|
309
|
+
return this.context.storage.sql
|
|
310
|
+
.exec<FundingSourceConfigurationRow>('SELECT value FROM configuration WHERE id = 1')
|
|
311
|
+
.toArray()[0]?.value
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async #ensureAlarm(): Promise<void> {
|
|
315
|
+
if ((await this.context.storage.getAlarm()) === null)
|
|
316
|
+
await this.context.storage.setAlarm(Date.now())
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
#readConfiguration(): FundingSourceConfiguration | undefined {
|
|
320
|
+
const value = this.#configuration()
|
|
321
|
+
return value ? (JSON.parse(value) as FundingSourceConfiguration) : undefined
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export declare namespace FundingSourceObserver {
|
|
326
|
+
/** Cloudflare bindings and deployment metadata used by the observer. */
|
|
327
|
+
type Environment = {
|
|
328
|
+
/** Deployment environment included in operational metrics. */
|
|
329
|
+
ENVIRONMENT?: string | undefined
|
|
330
|
+
/** Queue that receives deposit-address reconciliation work. */
|
|
331
|
+
FUNDING_RECONCILIATION_QUEUE: {
|
|
332
|
+
/** Enqueues one chain-detected address for reconciliation. */
|
|
333
|
+
send(message: Funding.Provider.Webhook.receive.Dispatchable): Promise<unknown>
|
|
334
|
+
}
|
|
335
|
+
/** Authoritative Postgres connection exposed through Hyperdrive. */
|
|
336
|
+
HYPERDRIVE: {
|
|
337
|
+
/** PostgreSQL connection string supplied by the Hyperdrive binding. */
|
|
338
|
+
connectionString: string
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function serializeFundingSourceConfiguration(options: FundingSourceConfiguration): string {
|
|
344
|
+
return JSON.stringify({
|
|
345
|
+
chainId: options.chainId,
|
|
346
|
+
rpcUrls: options.rpcUrls,
|
|
347
|
+
tokenAddress: options.tokenAddress.toLowerCase(),
|
|
348
|
+
} satisfies FundingSourceConfiguration)
|
|
349
|
+
}
|
package/src/db/Db.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type * as EarnVaults from './tables/earnVaults.js'
|
|
|
17
17
|
import type * as EnabledBillingSources from './tables/enabledBillingSources.js'
|
|
18
18
|
import type * as FundingCatalog from './tables/fundingCatalog.js'
|
|
19
19
|
import type * as FundingDepositAddresses from './tables/fundingDepositAddresses.js'
|
|
20
|
+
import type * as FundingDepositRequestObservations from './tables/fundingDepositRequestObservations.js'
|
|
20
21
|
import type * as FundingDeposits from './tables/fundingDeposits.js'
|
|
21
22
|
import type * as FundingIdempotency from './tables/fundingIdempotency.js'
|
|
22
23
|
import type * as FundingTransferEvents from './tables/fundingTransferEvents.js'
|
|
@@ -50,6 +51,7 @@ export type Database = {
|
|
|
50
51
|
funding_chain_tokens: FundingCatalog.ChainTokenTable
|
|
51
52
|
funding_chains: FundingCatalog.ChainTable
|
|
52
53
|
funding_deposit_addresses: FundingDepositAddresses.Table
|
|
54
|
+
funding_deposit_request_observations: FundingDepositRequestObservations.Table
|
|
53
55
|
funding_deposits: FundingDeposits.Table
|
|
54
56
|
funding_idempotency_requests: FundingIdempotency.Table
|
|
55
57
|
funding_routes: FundingCatalog.RouteTable
|
|
@@ -1209,6 +1211,66 @@ export const migrations: readonly Migration[] = [
|
|
|
1209
1211
|
$$`,
|
|
1210
1212
|
),
|
|
1211
1213
|
},
|
|
1214
|
+
{
|
|
1215
|
+
name: '0108_funding_deposits_recipient_index',
|
|
1216
|
+
// The expression indexes existing snapshots without a deployment-order-sensitive column backfill.
|
|
1217
|
+
up: (db) =>
|
|
1218
|
+
raw(
|
|
1219
|
+
db,
|
|
1220
|
+
sql`CREATE INDEX IF NOT EXISTS funding_deposits_recipient_idx ON funding_deposits (org_id, environment, lower(snapshot->>'recipient'), created_at, id)`,
|
|
1221
|
+
),
|
|
1222
|
+
},
|
|
1223
|
+
{
|
|
1224
|
+
name: '0109_funding_deposits_detection_trigger',
|
|
1225
|
+
up: (db) =>
|
|
1226
|
+
raw(
|
|
1227
|
+
db,
|
|
1228
|
+
sql`ALTER TABLE funding_deposits ADD COLUMN IF NOT EXISTS detection_trigger text CHECK (detection_trigger IN ('manual', 'poll', 'webhook'))`,
|
|
1229
|
+
),
|
|
1230
|
+
},
|
|
1231
|
+
{
|
|
1232
|
+
name: '0110_funding_deposit_request_observations',
|
|
1233
|
+
up: (db) =>
|
|
1234
|
+
db.schema
|
|
1235
|
+
.createTable('fundingDepositRequestObservations')
|
|
1236
|
+
.ifNotExists()
|
|
1237
|
+
.addColumn('depositAddressId', 'text', (c) =>
|
|
1238
|
+
c.notNull().references('fundingDepositAddresses.id').onDelete('cascade'),
|
|
1239
|
+
)
|
|
1240
|
+
.addColumn('pollObservedAt', 'text')
|
|
1241
|
+
.addColumn('providerRequestId', 'text', (c) => c.notNull())
|
|
1242
|
+
.addColumn('webhookReceivedAt', 'text')
|
|
1243
|
+
.addPrimaryKeyConstraint('funding_deposit_request_observations_pkey', [
|
|
1244
|
+
'depositAddressId',
|
|
1245
|
+
'providerRequestId',
|
|
1246
|
+
]),
|
|
1247
|
+
},
|
|
1248
|
+
{
|
|
1249
|
+
name: '0111_funding_deposit_observation_timestamps',
|
|
1250
|
+
up: (db) =>
|
|
1251
|
+
raw(
|
|
1252
|
+
db,
|
|
1253
|
+
sql`ALTER TABLE funding_deposits
|
|
1254
|
+
ADD COLUMN IF NOT EXISTS poll_observed_at text,
|
|
1255
|
+
ADD COLUMN IF NOT EXISTS webhook_received_at text`,
|
|
1256
|
+
),
|
|
1257
|
+
},
|
|
1258
|
+
{
|
|
1259
|
+
name: '0112_funding_deposits_source_observation_index',
|
|
1260
|
+
up: (db) =>
|
|
1261
|
+
raw(
|
|
1262
|
+
db,
|
|
1263
|
+
sql`CREATE UNIQUE INDEX IF NOT EXISTS funding_deposits_source_observation_idx ON funding_deposits (deposit_address_id, source_transaction_hash, source_transfer_index)`,
|
|
1264
|
+
),
|
|
1265
|
+
},
|
|
1266
|
+
{
|
|
1267
|
+
name: '0113_funding_deposits_chain_detection_trigger',
|
|
1268
|
+
up: (db) =>
|
|
1269
|
+
raw(
|
|
1270
|
+
db,
|
|
1271
|
+
sql`ALTER TABLE funding_deposits DROP CONSTRAINT IF EXISTS funding_deposits_detection_trigger_check, ADD CONSTRAINT funding_deposits_detection_trigger_check CHECK (detection_trigger IN ('chain', 'manual', 'poll', 'webhook'))`,
|
|
1272
|
+
),
|
|
1273
|
+
},
|
|
1212
1274
|
]
|
|
1213
1275
|
|
|
1214
1276
|
/**
|
package/src/db/Schema.ts
CHANGED
|
@@ -517,6 +517,21 @@ export const FundingDepositAddress = z.object({
|
|
|
517
517
|
/** Columns of the `funding_deposit_addresses` table. */
|
|
518
518
|
export type FundingDepositAddress = z.output<typeof FundingDepositAddress>
|
|
519
519
|
|
|
520
|
+
/** First Tempo observations of one provider request through polling and webhooks. */
|
|
521
|
+
export const FundingDepositRequestObservation = z.object({
|
|
522
|
+
depositAddressId: z.string().check(z.describe('Funding deposit address id (`fda_…`).')),
|
|
523
|
+
pollObservedAt: z
|
|
524
|
+
.nullable(z.string())
|
|
525
|
+
.check(z.describe('When polling first observed the provider request, or null.')),
|
|
526
|
+
providerRequestId: z.string().check(z.describe('Provider request identifier.')),
|
|
527
|
+
webhookReceivedAt: z
|
|
528
|
+
.nullable(z.string())
|
|
529
|
+
.check(z.describe('When Tempo first received an authenticated webhook, or null.')),
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
/** Columns of the `funding_deposit_request_observations` table. */
|
|
533
|
+
export type FundingDepositRequestObservation = z.output<typeof FundingDepositRequestObservation>
|
|
534
|
+
|
|
520
535
|
/**
|
|
521
536
|
* One provider-observed deposit. Public route and evidence live in `snapshot`;
|
|
522
537
|
* reconciliation and settlement state is private.
|
|
@@ -524,11 +539,17 @@ export type FundingDepositAddress = z.output<typeof FundingDepositAddress>
|
|
|
524
539
|
export const FundingDeposit = z.object({
|
|
525
540
|
createdAt: z.string().check(z.describe('When the deposit was detected (ISO 8601).')),
|
|
526
541
|
depositAddressId: z.string().check(z.describe('Funding deposit address id (`fda_…`).')),
|
|
542
|
+
detectionTrigger: z
|
|
543
|
+
.nullable(z.custom<funding_Deposit.DetectionTrigger>())
|
|
544
|
+
.check(z.describe('Mechanism that first detected the deposit, or null for legacy rows.')),
|
|
527
545
|
environment: z
|
|
528
546
|
.enum(['production', 'sandbox'])
|
|
529
547
|
.check(z.describe('Key environment inherited from the deposit address.')),
|
|
530
548
|
id: z.string().check(z.describe('Funding deposit id (`fdp_…`).')),
|
|
531
549
|
orgId: z.string().check(z.describe('Owning organization id (`org_…`).')),
|
|
550
|
+
pollObservedAt: z
|
|
551
|
+
.nullable(z.string())
|
|
552
|
+
.check(z.describe('When polling first observed the provider request, or null.')),
|
|
532
553
|
projectId: z.nullable(z.string()).check(z.describe('Attributed project id (`prj_…`), or null.')),
|
|
533
554
|
providerOutputAmount: z
|
|
534
555
|
.nullable(z.custom<z.output<typeof internal_Schema.TokenAmount>>())
|
|
@@ -580,6 +601,9 @@ export const FundingDeposit = z.object({
|
|
|
580
601
|
tempoGasPaid: z.nullable(z.string()).check(z.describe('Tempo gas paid for settlement, or null.')),
|
|
581
602
|
updatedAt: z.string().check(z.describe('When the deposit last materially changed (ISO 8601).')),
|
|
582
603
|
version: z.number().check(z.int(), z.describe('Material version used to guard updates.')),
|
|
604
|
+
webhookReceivedAt: z
|
|
605
|
+
.nullable(z.string())
|
|
606
|
+
.check(z.describe('When Tempo first received an authenticated provider webhook, or null.')),
|
|
583
607
|
})
|
|
584
608
|
|
|
585
609
|
/** Columns of the `funding_deposits` table. */
|
|
@@ -168,6 +168,34 @@ export function getMatching(db: Db.Db, match: Match): Promise<Record | undefined
|
|
|
168
168
|
return query.executeTakeFirst()
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
/** Lists active provider addresses for one source-chain token. */
|
|
172
|
+
export function listActiveSources(
|
|
173
|
+
db: Db.Db,
|
|
174
|
+
options: listActiveSources.Options,
|
|
175
|
+
): Promise<readonly Record[]> {
|
|
176
|
+
return db.kysely
|
|
177
|
+
.selectFrom('funding_deposit_addresses')
|
|
178
|
+
.selectAll()
|
|
179
|
+
.where('providerId', '=', options.providerId)
|
|
180
|
+
.where('sourceChainId', '=', options.sourceChainId)
|
|
181
|
+
.where('sourceTokenKey', '=', options.sourceTokenKey)
|
|
182
|
+
.where('status', '=', 'active')
|
|
183
|
+
.orderBy('id', 'asc')
|
|
184
|
+
.execute()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export declare namespace listActiveSources {
|
|
188
|
+
/** Provider and source asset identifying one observer shard. */
|
|
189
|
+
type Options = {
|
|
190
|
+
/** Funding provider that owns the deposit addresses. */
|
|
191
|
+
providerId: string
|
|
192
|
+
/** Source chain CAIP-2 id. */
|
|
193
|
+
sourceChainId: string
|
|
194
|
+
/** Source token key. */
|
|
195
|
+
sourceTokenKey: string
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
171
199
|
/** Claims a bounded batch of active addresses due for provider reconciliation. */
|
|
172
200
|
export function claimDue(db: Db.Db, options: claimDue.Options): Promise<readonly Record[]> {
|
|
173
201
|
return db.transaction(async (tx) => {
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as DepositAddress from '../../internal/funding/DepositAddress.js'
|
|
2
|
+
import * as TestApp from '../../../test/App.js'
|
|
3
|
+
import * as TestFunding from '../../../test/Funding.js'
|
|
4
|
+
import * as FundingDepositRequestObservations from './fundingDepositRequestObservations.js'
|
|
5
|
+
|
|
6
|
+
const db = TestApp.database()
|
|
7
|
+
|
|
8
|
+
let addressCounter = 0
|
|
9
|
+
function seedAddress() {
|
|
10
|
+
const index = addressCounter++
|
|
11
|
+
return DepositAddress.create(db, {
|
|
12
|
+
apiKeyId: 'key_request_observations',
|
|
13
|
+
deliveryStrategy: 'provider',
|
|
14
|
+
environment: 'production',
|
|
15
|
+
now: new Date(Date.UTC(2026, 0, 1, 0, 0, index)),
|
|
16
|
+
orgId: 'org_request_observations',
|
|
17
|
+
providerOutputToken: TestFunding.providerOutputToken(),
|
|
18
|
+
snapshot: TestFunding.depositAddressSnapshot({
|
|
19
|
+
provider: { id: `relay_observation_${index}`, name: 'Relay' },
|
|
20
|
+
recipient: `0x${index.toString(16).padStart(40, '0')}`,
|
|
21
|
+
}),
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('get', () => {
|
|
26
|
+
test('returns no observation before a request is seen', async () => {
|
|
27
|
+
const address = await seedAddress()
|
|
28
|
+
expect(
|
|
29
|
+
await FundingDepositRequestObservations.get(db, {
|
|
30
|
+
depositAddressId: address.id,
|
|
31
|
+
providerRequestId: 'relay_unseen',
|
|
32
|
+
}),
|
|
33
|
+
).toMatchInlineSnapshot(`undefined`)
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('observePoll', () => {
|
|
38
|
+
test('retains the first poll timestamp for each request', async () => {
|
|
39
|
+
const address = await seedAddress()
|
|
40
|
+
await FundingDepositRequestObservations.observePoll(db, {
|
|
41
|
+
depositAddressId: address.id,
|
|
42
|
+
observedAt: '2026-01-01T00:00:10.000Z',
|
|
43
|
+
providerRequestIds: ['relay_poll', 'relay_poll'],
|
|
44
|
+
})
|
|
45
|
+
await FundingDepositRequestObservations.observePoll(db, {
|
|
46
|
+
depositAddressId: address.id,
|
|
47
|
+
observedAt: '2026-01-01T00:00:09.000Z',
|
|
48
|
+
providerRequestIds: ['relay_poll'],
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
expect(
|
|
52
|
+
await FundingDepositRequestObservations.get(db, {
|
|
53
|
+
depositAddressId: address.id,
|
|
54
|
+
providerRequestId: 'relay_poll',
|
|
55
|
+
}),
|
|
56
|
+
).toMatchInlineSnapshot(
|
|
57
|
+
{ depositAddressId: expect.any(String) },
|
|
58
|
+
`
|
|
59
|
+
{
|
|
60
|
+
"depositAddressId": Any<String>,
|
|
61
|
+
"pollObservedAt": "2026-01-01T00:00:09.000Z",
|
|
62
|
+
"providerRequestId": "relay_poll",
|
|
63
|
+
"webhookReceivedAt": null,
|
|
64
|
+
}
|
|
65
|
+
`,
|
|
66
|
+
)
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe('observeWebhook', () => {
|
|
71
|
+
test('retains both first-observation timestamps regardless of arrival order', async () => {
|
|
72
|
+
const address = await seedAddress()
|
|
73
|
+
await FundingDepositRequestObservations.observeWebhook(db, {
|
|
74
|
+
depositAddressId: address.id,
|
|
75
|
+
providerRequestId: 'relay_webhook',
|
|
76
|
+
receivedAt: '2026-01-01T00:00:08.000Z',
|
|
77
|
+
})
|
|
78
|
+
await FundingDepositRequestObservations.observePoll(db, {
|
|
79
|
+
depositAddressId: address.id,
|
|
80
|
+
observedAt: '2026-01-01T00:00:07.000Z',
|
|
81
|
+
providerRequestIds: ['relay_webhook'],
|
|
82
|
+
})
|
|
83
|
+
await FundingDepositRequestObservations.observeWebhook(db, {
|
|
84
|
+
depositAddressId: address.id,
|
|
85
|
+
providerRequestId: 'relay_webhook',
|
|
86
|
+
receivedAt: '2026-01-01T00:00:06.000Z',
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
expect(
|
|
90
|
+
await FundingDepositRequestObservations.get(db, {
|
|
91
|
+
depositAddressId: address.id,
|
|
92
|
+
providerRequestId: 'relay_webhook',
|
|
93
|
+
}),
|
|
94
|
+
).toMatchInlineSnapshot(
|
|
95
|
+
{ depositAddressId: expect.any(String) },
|
|
96
|
+
`
|
|
97
|
+
{
|
|
98
|
+
"depositAddressId": Any<String>,
|
|
99
|
+
"pollObservedAt": "2026-01-01T00:00:07.000Z",
|
|
100
|
+
"providerRequestId": "relay_webhook",
|
|
101
|
+
"webhookReceivedAt": "2026-01-01T00:00:06.000Z",
|
|
102
|
+
}
|
|
103
|
+
`,
|
|
104
|
+
)
|
|
105
|
+
})
|
|
106
|
+
})
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { sql, type Selectable } from 'kysely'
|
|
2
|
+
|
|
3
|
+
import type * as Db from '../Db.js'
|
|
4
|
+
import type * as db_Schema from '../Schema.js'
|
|
5
|
+
|
|
6
|
+
/** Columns of the `funding_deposit_request_observations` table. */
|
|
7
|
+
export type Table = db_Schema.FundingDepositRequestObservation
|
|
8
|
+
|
|
9
|
+
/** First Tempo observations of one provider request. */
|
|
10
|
+
export type Record = Selectable<Table>
|
|
11
|
+
|
|
12
|
+
/** Gets observation timestamps for one provider request. */
|
|
13
|
+
export function get(db: Db.Db, options: get.Options): Promise<Record | undefined> {
|
|
14
|
+
return db.kysely
|
|
15
|
+
.selectFrom('funding_deposit_request_observations')
|
|
16
|
+
.selectAll()
|
|
17
|
+
.where('depositAddressId', '=', options.depositAddressId)
|
|
18
|
+
.where('providerRequestId', '=', options.providerRequestId)
|
|
19
|
+
.executeTakeFirst()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export declare namespace get {
|
|
23
|
+
/** Provider request identity scoped to one reusable address. */
|
|
24
|
+
type Options = {
|
|
25
|
+
/** Funding deposit address id (`fda_…`). */
|
|
26
|
+
depositAddressId: string
|
|
27
|
+
/** Provider request identifier. */
|
|
28
|
+
providerRequestId: string
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Records when polling first observed each provider request. */
|
|
33
|
+
export async function observePoll(db: Db.Db, options: observePoll.Options): Promise<Record[]> {
|
|
34
|
+
const providerRequestIds = [...new Set(options.providerRequestIds)]
|
|
35
|
+
if (providerRequestIds.length === 0) return []
|
|
36
|
+
return db.kysely
|
|
37
|
+
.insertInto('funding_deposit_request_observations')
|
|
38
|
+
.values(
|
|
39
|
+
providerRequestIds.map((providerRequestId) => ({
|
|
40
|
+
depositAddressId: options.depositAddressId,
|
|
41
|
+
pollObservedAt: options.observedAt,
|
|
42
|
+
providerRequestId,
|
|
43
|
+
webhookReceivedAt: null,
|
|
44
|
+
})),
|
|
45
|
+
)
|
|
46
|
+
.onConflict((oc) =>
|
|
47
|
+
oc.columns(['depositAddressId', 'providerRequestId']).doUpdateSet({
|
|
48
|
+
pollObservedAt: sql<string>`LEAST(COALESCE(funding_deposit_request_observations.poll_observed_at, excluded.poll_observed_at), excluded.poll_observed_at)`,
|
|
49
|
+
}),
|
|
50
|
+
)
|
|
51
|
+
.returningAll()
|
|
52
|
+
.execute()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export declare namespace observePoll {
|
|
56
|
+
/** Provider requests observed in one successful poll response. */
|
|
57
|
+
type Options = {
|
|
58
|
+
/** Funding deposit address id (`fda_…`). */
|
|
59
|
+
depositAddressId: string
|
|
60
|
+
/** When Tempo observed the provider requests. */
|
|
61
|
+
observedAt: string
|
|
62
|
+
/** Provider request identifiers returned by the poll. */
|
|
63
|
+
providerRequestIds: readonly string[]
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Records when Tempo first received an authenticated webhook for a provider request. */
|
|
68
|
+
export function observeWebhook(
|
|
69
|
+
db: Db.Db,
|
|
70
|
+
options: observeWebhook.Options,
|
|
71
|
+
): Promise<Record | undefined> {
|
|
72
|
+
return db.kysely
|
|
73
|
+
.insertInto('funding_deposit_request_observations')
|
|
74
|
+
.values({
|
|
75
|
+
depositAddressId: options.depositAddressId,
|
|
76
|
+
pollObservedAt: null,
|
|
77
|
+
providerRequestId: options.providerRequestId,
|
|
78
|
+
webhookReceivedAt: options.receivedAt,
|
|
79
|
+
})
|
|
80
|
+
.onConflict((oc) =>
|
|
81
|
+
oc.columns(['depositAddressId', 'providerRequestId']).doUpdateSet({
|
|
82
|
+
webhookReceivedAt: sql<string>`LEAST(COALESCE(funding_deposit_request_observations.webhook_received_at, excluded.webhook_received_at), excluded.webhook_received_at)`,
|
|
83
|
+
}),
|
|
84
|
+
)
|
|
85
|
+
.returningAll()
|
|
86
|
+
.executeTakeFirst()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export declare namespace observeWebhook {
|
|
90
|
+
/** Authenticated webhook observation for one provider request. */
|
|
91
|
+
type Options = {
|
|
92
|
+
/** Funding deposit address id (`fda_…`). */
|
|
93
|
+
depositAddressId: string
|
|
94
|
+
/** Provider request identifier. */
|
|
95
|
+
providerRequestId: string
|
|
96
|
+
/** When Tempo received the webhook. */
|
|
97
|
+
receivedAt: string
|
|
98
|
+
}
|
|
99
|
+
}
|