tapimo 0.10.1 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/App.d.ts +1776 -1543
- package/dist/App.d.ts.map +1 -1
- package/dist/Webhooks.d.ts +1 -1
- package/dist/Webhooks.d.ts.map +1 -1
- package/dist/Webhooks.js +1 -1
- package/dist/Webhooks.js.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +4 -4
- package/dist/analytics/Analytics.d.ts.map +1 -1
- package/dist/analytics/Analytics.js +206 -0
- package/dist/analytics/Analytics.js.map +1 -1
- package/dist/analytics/tables/requestEvents.d.ts +4 -0
- package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
- package/dist/analytics/tables/requestEvents.js +2 -0
- package/dist/analytics/tables/requestEvents.js.map +1 -1
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +2 -0
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/routes/addresses.d.ts +340 -340
- package/dist/apps/data/routes/balances.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.js +2 -6
- package/dist/apps/data/routes/balances.js.map +1 -1
- package/dist/apps/data/routes/coingecko.d.ts +176 -176
- package/dist/apps/data/routes/earn.d.ts.map +1 -1
- package/dist/apps/data/routes/earn.js +63 -34
- package/dist/apps/data/routes/earn.js.map +1 -1
- package/dist/apps/data/routes/exchanges.d.ts +4 -0
- package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
- package/dist/apps/data/routes/exchanges.js +115 -77
- package/dist/apps/data/routes/exchanges.js.map +1 -1
- package/dist/apps/data/routes/indexer.d.ts +34 -34
- package/dist/apps/data/routes/receipts.d.ts +4 -3
- package/dist/apps/data/routes/receipts.d.ts.map +1 -1
- package/dist/apps/data/routes/receipts.js +15 -14
- package/dist/apps/data/routes/receipts.js.map +1 -1
- package/dist/apps/data/routes/tokens.d.ts +1 -1
- package/dist/apps/data/routes/tokens.d.ts.map +1 -1
- package/dist/apps/data/routes/tokens.js +4 -3
- package/dist/apps/data/routes/tokens.js.map +1 -1
- package/dist/apps/data/routes/transactions.d.ts +281 -281
- package/dist/apps/data/routes/transfers.d.ts.map +1 -1
- package/dist/apps/data/routes/transfers.js +25 -7
- package/dist/apps/data/routes/transfers.js.map +1 -1
- package/dist/apps/data/routes/webhooks.d.ts +380 -364
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +16 -9
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/data/routes/zones.d.ts +31 -31
- package/dist/apps/funding/App.d.ts +233 -0
- package/dist/apps/funding/App.d.ts.map +1 -1
- package/dist/apps/funding/routes/deposit-addresses.d.ts +330 -0
- package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
- package/dist/apps/funding/routes/deposit-addresses.js +207 -11
- package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
- package/dist/apps/funding/routes/providers.d.ts +13 -13
- package/dist/apps/management/routes/faucet.d.ts +32 -32
- package/dist/apps/management/routes/members.d.ts +79 -79
- package/dist/apps/management/routes/usage.d.ts +80 -80
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +32 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/tables/fundingDepositAddresses.d.ts +20 -0
- package/dist/db/tables/fundingDepositAddresses.d.ts.map +1 -1
- package/dist/db/tables/fundingDepositAddresses.js +17 -0
- package/dist/db/tables/fundingDepositAddresses.js.map +1 -1
- package/dist/db/tables/fundingDeposits.d.ts +41 -0
- package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
- package/dist/db/tables/fundingDeposits.js +69 -0
- package/dist/db/tables/fundingDeposits.js.map +1 -1
- package/dist/internal/Auth.d.ts +0 -26
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +0 -33
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/Credential.d.ts +27 -0
- package/dist/internal/Credential.d.ts.map +1 -0
- package/dist/internal/Credential.js +34 -0
- package/dist/internal/Credential.js.map +1 -0
- package/dist/internal/EarnRates.js +11 -7
- package/dist/internal/EarnRates.js.map +1 -1
- package/dist/internal/Log.d.ts +8 -0
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +4 -0
- package/dist/internal/Log.js.map +1 -1
- package/dist/internal/MetricSink.d.ts +24 -0
- package/dist/internal/MetricSink.d.ts.map +1 -1
- package/dist/internal/MetricSink.js +25 -0
- package/dist/internal/MetricSink.js.map +1 -1
- package/dist/internal/Tidx.d.ts +4 -3
- package/dist/internal/Tidx.d.ts.map +1 -1
- package/dist/internal/Tidx.js +2 -2
- package/dist/internal/Tidx.js.map +1 -1
- package/dist/internal/Viem.d.ts +4 -3
- package/dist/internal/Viem.d.ts.map +1 -1
- package/dist/internal/Viem.js +3 -2
- package/dist/internal/Viem.js.map +1 -1
- package/dist/internal/Webhooks.d.ts +2 -0
- package/dist/internal/Webhooks.d.ts.map +1 -1
- package/dist/internal/Webhooks.js +10 -3
- package/dist/internal/Webhooks.js.map +1 -1
- package/dist/internal/index.d.ts +1 -0
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +1 -0
- package/dist/internal/index.js.map +1 -1
- package/package.json +1 -1
- package/src/App.test.ts +30 -0
- package/src/Client.test-d.ts +33 -1
- package/src/Webhooks.ts +1 -1
- package/src/analytics/Analytics.test.ts +34 -0
- package/src/analytics/Analytics.ts +206 -0
- package/src/analytics/tables/requestEvents.ts +6 -0
- package/src/apps/composition.test.ts +1 -0
- package/src/apps/data/App.ts +2 -0
- package/src/apps/data/routes/balances.ts +2 -6
- package/src/apps/data/routes/earn.ts +64 -34
- package/src/apps/data/routes/exchanges.test.ts +8 -3
- package/src/apps/data/routes/exchanges.ts +134 -82
- package/src/apps/data/routes/receipts.ts +16 -14
- package/src/apps/data/routes/tokens.ts +3 -2
- package/src/apps/data/routes/transactions.bench.ts +8 -0
- package/src/apps/data/routes/transfers.ts +36 -10
- package/src/apps/data/routes/webhooks.ts +31 -10
- package/src/apps/funding/routes/deposit-addresses.test.ts +216 -2
- package/src/apps/funding/routes/deposit-addresses.ts +254 -18
- package/src/apps/funding/routes/transfers.test.ts +17 -2
- package/src/db/Db.ts +36 -0
- package/src/db/tables/fundingDepositAddresses.test.ts +28 -0
- package/src/db/tables/fundingDepositAddresses.ts +39 -0
- package/src/db/tables/fundingDeposits.test.ts +82 -0
- package/src/db/tables/fundingDeposits.ts +130 -0
- package/src/internal/Auth.test.ts +0 -48
- package/src/internal/Auth.ts +0 -59
- package/src/internal/Credential.test.ts +51 -0
- package/src/internal/Credential.ts +58 -0
- package/src/internal/EarnRates.ts +13 -7
- package/src/internal/Log.test.ts +3 -1
- package/src/internal/Log.ts +14 -0
- package/src/internal/MetricSink.test.ts +50 -1
- package/src/internal/MetricSink.ts +52 -0
- package/src/internal/Tidx.ts +5 -4
- package/src/internal/Viem.test.ts +4 -0
- package/src/internal/Viem.ts +6 -4
- package/src/internal/Webhooks.test.ts +9 -0
- package/src/internal/Webhooks.ts +10 -3
- package/src/internal/index.test.ts +1 -0
- package/src/internal/index.ts +1 -0
|
@@ -27,6 +27,24 @@ export type Owner = {
|
|
|
27
27
|
projectId?: string | undefined
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/** Optional filters shared by funding deposit list and count queries. */
|
|
31
|
+
export type Filters = {
|
|
32
|
+
/** Reusable source-chain address that received the funds. */
|
|
33
|
+
depositAddress?: string | undefined
|
|
34
|
+
/** Canonical destination token keys accepted by the filter. */
|
|
35
|
+
destinationTokenKeys?: readonly string[] | undefined
|
|
36
|
+
/** Funding provider id. */
|
|
37
|
+
providerId?: string | undefined
|
|
38
|
+
/** Tempo account that receives completed deposits. */
|
|
39
|
+
recipient?: string | undefined
|
|
40
|
+
/** Canonical source chain ids accepted by the filter. */
|
|
41
|
+
sourceChainIds?: readonly string[] | undefined
|
|
42
|
+
/** Canonical source token keys accepted by the filter. */
|
|
43
|
+
sourceTokenKeys?: readonly string[] | undefined
|
|
44
|
+
/** Deposit lifecycle status. */
|
|
45
|
+
status?: Record['status'] | undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
30
48
|
/** Inserts one provider-observed source transfer as a funding deposit. */
|
|
31
49
|
export function insert(db: Db.Db, record: Record): Promise<Record> {
|
|
32
50
|
return db.kysely
|
|
@@ -234,6 +252,118 @@ export function getOwned(db: Db.Db, owner: Owner, id: string): Promise<Record |
|
|
|
234
252
|
return query.executeTakeFirst()
|
|
235
253
|
}
|
|
236
254
|
|
|
255
|
+
/** Lists owner-visible funding deposits newest-first. */
|
|
256
|
+
export function list(db: Db.Db, options: list.Options): Promise<Record[]> {
|
|
257
|
+
const { cursor, limit } = options
|
|
258
|
+
let builder = query(db, options).selectAll('funding_deposits')
|
|
259
|
+
if (cursor)
|
|
260
|
+
builder = builder.where((eb) =>
|
|
261
|
+
eb.or([
|
|
262
|
+
eb('funding_deposits.createdAt', '<', cursor.createdAt),
|
|
263
|
+
eb.and([
|
|
264
|
+
eb('funding_deposits.createdAt', '=', cursor.createdAt),
|
|
265
|
+
eb('funding_deposits.id', '<', cursor.id),
|
|
266
|
+
]),
|
|
267
|
+
]),
|
|
268
|
+
)
|
|
269
|
+
return builder
|
|
270
|
+
.orderBy('funding_deposits.createdAt', 'desc')
|
|
271
|
+
.orderBy('funding_deposits.id', 'desc')
|
|
272
|
+
.limit(limit)
|
|
273
|
+
.execute()
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export declare namespace list {
|
|
277
|
+
/** Ownership and paging options for funding deposits. */
|
|
278
|
+
type Options = Filters & {
|
|
279
|
+
/** Exclusive lower bound from the previous page. */
|
|
280
|
+
cursor?: listByAddress.Cursor | undefined
|
|
281
|
+
/** Maximum rows to return. */
|
|
282
|
+
limit: number
|
|
283
|
+
/** Ownership filter derived from the authenticated API key. */
|
|
284
|
+
owner: Owner
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Counts owner-visible funding deposits through the list's shared filters. */
|
|
289
|
+
export async function count(db: Db.Db, options: count.Options): Promise<number> {
|
|
290
|
+
const rows = query(db, options)
|
|
291
|
+
.select('funding_deposits.id')
|
|
292
|
+
.limit(options.cap + 1)
|
|
293
|
+
.as('rows')
|
|
294
|
+
const result = await db.kysely
|
|
295
|
+
.selectFrom(rows)
|
|
296
|
+
.select(sql<number>`count(*)::int`.as('count'))
|
|
297
|
+
.executeTakeFirstOrThrow()
|
|
298
|
+
return result.count
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export declare namespace count {
|
|
302
|
+
/** Ownership, filter, and cap options for counting funding deposits. */
|
|
303
|
+
type Options = Filters & {
|
|
304
|
+
/** Maximum exact count before returning the cap plus one. */
|
|
305
|
+
cap: number
|
|
306
|
+
/** Ownership filter derived from the authenticated API key. */
|
|
307
|
+
owner: Owner
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function query(db: Db.Db, options: query.Options) {
|
|
312
|
+
const owner = options.owner
|
|
313
|
+
let builder = db.kysely
|
|
314
|
+
.selectFrom('funding_deposits')
|
|
315
|
+
.innerJoin(
|
|
316
|
+
'funding_deposit_addresses',
|
|
317
|
+
'funding_deposit_addresses.id',
|
|
318
|
+
'funding_deposits.depositAddressId',
|
|
319
|
+
)
|
|
320
|
+
.where('funding_deposit_addresses.environment', '=', owner.environment)
|
|
321
|
+
.where('funding_deposit_addresses.orgId', '=', owner.orgId)
|
|
322
|
+
.where('funding_deposits.environment', '=', owner.environment)
|
|
323
|
+
.where('funding_deposits.orgId', '=', owner.orgId)
|
|
324
|
+
if (owner.projectId !== undefined)
|
|
325
|
+
builder = builder
|
|
326
|
+
.where('funding_deposit_addresses.projectId', '=', owner.projectId)
|
|
327
|
+
.where('funding_deposits.projectId', '=', owner.projectId)
|
|
328
|
+
if (options.depositAddress !== undefined) {
|
|
329
|
+
const address = /^0x[0-9a-fA-F]{40}$/.test(options.depositAddress)
|
|
330
|
+
? options.depositAddress.toLowerCase()
|
|
331
|
+
: options.depositAddress
|
|
332
|
+
builder = builder.where('funding_deposit_addresses.address', '=', address)
|
|
333
|
+
}
|
|
334
|
+
if (options.destinationTokenKeys !== undefined)
|
|
335
|
+
builder = options.destinationTokenKeys.length
|
|
336
|
+
? builder.where(
|
|
337
|
+
'funding_deposit_addresses.destinationTokenKey',
|
|
338
|
+
'in',
|
|
339
|
+
options.destinationTokenKeys,
|
|
340
|
+
)
|
|
341
|
+
: builder.where(sql<boolean>`false`)
|
|
342
|
+
if (options.providerId !== undefined)
|
|
343
|
+
builder = builder.where('funding_deposit_addresses.providerId', '=', options.providerId)
|
|
344
|
+
if (options.recipient !== undefined)
|
|
345
|
+
builder = builder.where(
|
|
346
|
+
sql<boolean>`lower(funding_deposit_addresses.recipient) = ${options.recipient.toLowerCase()}`,
|
|
347
|
+
)
|
|
348
|
+
if (options.sourceChainIds !== undefined)
|
|
349
|
+
builder = options.sourceChainIds.length
|
|
350
|
+
? builder.where('funding_deposit_addresses.sourceChainId', 'in', options.sourceChainIds)
|
|
351
|
+
: builder.where(sql<boolean>`false`)
|
|
352
|
+
if (options.sourceTokenKeys !== undefined)
|
|
353
|
+
builder = options.sourceTokenKeys.length
|
|
354
|
+
? builder.where('funding_deposit_addresses.sourceTokenKey', 'in', options.sourceTokenKeys)
|
|
355
|
+
: builder.where(sql<boolean>`false`)
|
|
356
|
+
if (options.status !== undefined)
|
|
357
|
+
builder = builder.where('funding_deposits.status', '=', options.status)
|
|
358
|
+
return builder
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
declare namespace query {
|
|
362
|
+
type Options = Filters & {
|
|
363
|
+
owner: Owner
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
237
367
|
/** Lists one owner-visible address's deposits newest-first. */
|
|
238
368
|
export function listByAddress(
|
|
239
369
|
db: Db.Db,
|
|
@@ -2100,54 +2100,6 @@ describe('ensureProject', () => {
|
|
|
2100
2100
|
})
|
|
2101
2101
|
})
|
|
2102
2102
|
|
|
2103
|
-
describe('parseCredential', () => {
|
|
2104
|
-
test('parses credentials keyed by chain id', () => {
|
|
2105
|
-
const credential = Auth.parseCredential(
|
|
2106
|
-
JSON.stringify({ 4217: 'mainnet:secret', 421700001: 'zone:secret' }),
|
|
2107
|
-
)
|
|
2108
|
-
|
|
2109
|
-
expect(credential).toEqual({ 4217: 'mainnet:secret', 421700001: 'zone:secret' })
|
|
2110
|
-
})
|
|
2111
|
-
|
|
2112
|
-
test('supports a shared credential', () => {
|
|
2113
|
-
expect(Auth.parseCredential('shared:secret')).toBe('shared:secret')
|
|
2114
|
-
})
|
|
2115
|
-
|
|
2116
|
-
test('rejects malformed credential maps', () => {
|
|
2117
|
-
expect(() => Auth.parseCredential('{"4217":1}')).toThrowErrorMatchingInlineSnapshot(
|
|
2118
|
-
`[Error: Expected a credential or a chain-id-to-credential JSON object.]`,
|
|
2119
|
-
)
|
|
2120
|
-
})
|
|
2121
|
-
})
|
|
2122
|
-
|
|
2123
|
-
describe('resolveCredential', () => {
|
|
2124
|
-
test('infers Basic auth from a credential containing a colon', () => {
|
|
2125
|
-
expect(Auth.resolveCredential({ chainId: 4217, value: 'user:password' })).toEqual({
|
|
2126
|
-
basicAuth: 'user:password',
|
|
2127
|
-
})
|
|
2128
|
-
})
|
|
2129
|
-
|
|
2130
|
-
test('infers Bearer auth from a credential without a colon', () => {
|
|
2131
|
-
expect(Auth.resolveCredential({ chainId: 4217, value: 'bearer-token' })).toEqual({
|
|
2132
|
-
basicAuth: undefined,
|
|
2133
|
-
bearerAuth: 'bearer-token',
|
|
2134
|
-
})
|
|
2135
|
-
})
|
|
2136
|
-
|
|
2137
|
-
test('selects a credential by chain id', () => {
|
|
2138
|
-
const value = JSON.stringify({ 4217: 'mainnet:secret', 421700001: 'zone-token' })
|
|
2139
|
-
|
|
2140
|
-
expect(Auth.resolveCredential({ chainId: 4217, value })).toEqual({
|
|
2141
|
-
basicAuth: 'mainnet:secret',
|
|
2142
|
-
})
|
|
2143
|
-
expect(Auth.resolveCredential({ chainId: 421700001, value })).toEqual({
|
|
2144
|
-
basicAuth: undefined,
|
|
2145
|
-
bearerAuth: 'zone-token',
|
|
2146
|
-
})
|
|
2147
|
-
expect(Auth.resolveCredential({ chainId: 42431, value })).toEqual({ basicAuth: undefined })
|
|
2148
|
-
})
|
|
2149
|
-
})
|
|
2150
|
-
|
|
2151
2103
|
function createScopedApp(options: {
|
|
2152
2104
|
auth?: Auth.Context | undefined
|
|
2153
2105
|
routes: readonly { path: string; policy: Auth.require.Policy }[]
|
package/src/internal/Auth.ts
CHANGED
|
@@ -1674,65 +1674,6 @@ type RateLimitAccess = PaymentAccess & {
|
|
|
1674
1674
|
rateLimit: RateLimit.Result
|
|
1675
1675
|
}
|
|
1676
1676
|
|
|
1677
|
-
/** Shared or per-chain credentials. */
|
|
1678
|
-
export type ChainCredential = string | Record<number, string>
|
|
1679
|
-
|
|
1680
|
-
/** Parses shared or per-chain credentials from an environment value. */
|
|
1681
|
-
export function parseCredential(value: ChainCredential | undefined): parseCredential.ReturnType {
|
|
1682
|
-
if (!value) return undefined
|
|
1683
|
-
if (typeof value !== 'string') return value
|
|
1684
|
-
const credentials = (() => {
|
|
1685
|
-
try {
|
|
1686
|
-
return JSON.parse(value) as unknown
|
|
1687
|
-
} catch {
|
|
1688
|
-
return value
|
|
1689
|
-
}
|
|
1690
|
-
})()
|
|
1691
|
-
if (typeof credentials === 'string') return credentials
|
|
1692
|
-
if (
|
|
1693
|
-
!credentials ||
|
|
1694
|
-
typeof credentials !== 'object' ||
|
|
1695
|
-
Array.isArray(credentials) ||
|
|
1696
|
-
Object.values(credentials).some((credential) => typeof credential !== 'string')
|
|
1697
|
-
)
|
|
1698
|
-
throw new Error('Expected a credential or a chain-id-to-credential JSON object.')
|
|
1699
|
-
return credentials as Record<number, string>
|
|
1700
|
-
}
|
|
1701
|
-
|
|
1702
|
-
export declare namespace parseCredential {
|
|
1703
|
-
/** Parsed shared or per-chain credentials. */
|
|
1704
|
-
type ReturnType = ChainCredential | undefined
|
|
1705
|
-
}
|
|
1706
|
-
|
|
1707
|
-
/** Resolves a chain credential and infers its HTTP authorization scheme. */
|
|
1708
|
-
export function resolveCredential(
|
|
1709
|
-
options: resolveCredential.Options,
|
|
1710
|
-
): resolveCredential.ReturnType {
|
|
1711
|
-
const credentials = parseCredential(options.value)
|
|
1712
|
-
const credential = typeof credentials === 'string' ? credentials : credentials?.[options.chainId]
|
|
1713
|
-
if (!credential) return { basicAuth: undefined }
|
|
1714
|
-
if (credential.includes(':')) return { basicAuth: credential }
|
|
1715
|
-
return { basicAuth: undefined, bearerAuth: credential }
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
|
-
export declare namespace resolveCredential {
|
|
1719
|
-
/** Options for resolving a chain credential. */
|
|
1720
|
-
type Options = {
|
|
1721
|
-
/** Chain id selecting a per-chain credential. */
|
|
1722
|
-
chainId: number
|
|
1723
|
-
/** Shared or per-chain credentials. */
|
|
1724
|
-
value?: ChainCredential | undefined
|
|
1725
|
-
}
|
|
1726
|
-
|
|
1727
|
-
/** Resolved HTTP authorization credentials. */
|
|
1728
|
-
type ReturnType = {
|
|
1729
|
-
/** Basic credentials inferred from a value containing a colon. */
|
|
1730
|
-
basicAuth: string | undefined
|
|
1731
|
-
/** Bearer credential inferred from a value without a colon. */
|
|
1732
|
-
bearerAuth?: string | undefined
|
|
1733
|
-
}
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
1677
|
/**
|
|
1737
1678
|
* How long to wait for the rate-limit counter before failing open. A hot
|
|
1738
1679
|
* counter backend (e.g. a single Durable Object serving one busy quota bucket)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as Credential from './Credential.js'
|
|
2
|
+
|
|
3
|
+
describe('parseCredential', () => {
|
|
4
|
+
test('parses credentials keyed by chain id', () => {
|
|
5
|
+
const credential = Credential.parseCredential(
|
|
6
|
+
JSON.stringify({ 4217: 'mainnet:secret', 421700001: 'zone:secret' }),
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
expect(credential).toEqual({ 4217: 'mainnet:secret', 421700001: 'zone:secret' })
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test('supports a shared credential', () => {
|
|
13
|
+
expect(Credential.parseCredential('shared:secret')).toBe('shared:secret')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('rejects malformed credential maps', () => {
|
|
17
|
+
expect(() => Credential.parseCredential('{"4217":1}')).toThrowErrorMatchingInlineSnapshot(
|
|
18
|
+
`[Error: Expected a credential or a chain-id-to-credential JSON object.]`,
|
|
19
|
+
)
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('resolveCredential', () => {
|
|
24
|
+
test('infers Basic auth from a credential containing a colon', () => {
|
|
25
|
+
expect(Credential.resolveCredential({ chainId: 4217, value: 'user:password' })).toEqual({
|
|
26
|
+
basicAuth: 'user:password',
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('infers Bearer auth from a credential without a colon', () => {
|
|
31
|
+
expect(Credential.resolveCredential({ chainId: 4217, value: 'bearer-token' })).toEqual({
|
|
32
|
+
basicAuth: undefined,
|
|
33
|
+
bearerAuth: 'bearer-token',
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('selects a credential by chain id', () => {
|
|
38
|
+
const value = JSON.stringify({ 4217: 'mainnet:secret', 421700001: 'zone-token' })
|
|
39
|
+
|
|
40
|
+
expect(Credential.resolveCredential({ chainId: 4217, value })).toEqual({
|
|
41
|
+
basicAuth: 'mainnet:secret',
|
|
42
|
+
})
|
|
43
|
+
expect(Credential.resolveCredential({ chainId: 421700001, value })).toEqual({
|
|
44
|
+
basicAuth: undefined,
|
|
45
|
+
bearerAuth: 'zone-token',
|
|
46
|
+
})
|
|
47
|
+
expect(Credential.resolveCredential({ chainId: 42431, value })).toEqual({
|
|
48
|
+
basicAuth: undefined,
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Shared or per-chain credentials. */
|
|
2
|
+
export type ChainCredential = string | Record<number, string>
|
|
3
|
+
|
|
4
|
+
/** Parses shared or per-chain credentials from an environment value. */
|
|
5
|
+
export function parseCredential(value: ChainCredential | undefined): parseCredential.ReturnType {
|
|
6
|
+
if (!value) return undefined
|
|
7
|
+
if (typeof value !== 'string') return value
|
|
8
|
+
const credentials = (() => {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(value) as unknown
|
|
11
|
+
} catch {
|
|
12
|
+
return value
|
|
13
|
+
}
|
|
14
|
+
})()
|
|
15
|
+
if (typeof credentials === 'string') return credentials
|
|
16
|
+
if (
|
|
17
|
+
!credentials ||
|
|
18
|
+
typeof credentials !== 'object' ||
|
|
19
|
+
Array.isArray(credentials) ||
|
|
20
|
+
Object.values(credentials).some((credential) => typeof credential !== 'string')
|
|
21
|
+
)
|
|
22
|
+
throw new Error('Expected a credential or a chain-id-to-credential JSON object.')
|
|
23
|
+
return credentials as Record<number, string>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export declare namespace parseCredential {
|
|
27
|
+
/** Parsed shared or per-chain credentials. */
|
|
28
|
+
type ReturnType = ChainCredential | undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Resolves a chain credential and infers its HTTP authorization scheme. */
|
|
32
|
+
export function resolveCredential(
|
|
33
|
+
options: resolveCredential.Options,
|
|
34
|
+
): resolveCredential.ReturnType {
|
|
35
|
+
const credentials = parseCredential(options.value)
|
|
36
|
+
const credential = typeof credentials === 'string' ? credentials : credentials?.[options.chainId]
|
|
37
|
+
if (!credential) return { basicAuth: undefined }
|
|
38
|
+
if (credential.includes(':')) return { basicAuth: credential }
|
|
39
|
+
return { basicAuth: undefined, bearerAuth: credential }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export declare namespace resolveCredential {
|
|
43
|
+
/** Options for resolving a chain credential. */
|
|
44
|
+
type Options = {
|
|
45
|
+
/** Chain id selecting a per-chain credential. */
|
|
46
|
+
chainId: number
|
|
47
|
+
/** Shared or per-chain credentials. */
|
|
48
|
+
value?: ChainCredential | undefined
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Resolved HTTP authorization credentials. */
|
|
52
|
+
type ReturnType = {
|
|
53
|
+
/** Basic credentials inferred from a value containing a colon. */
|
|
54
|
+
basicAuth: string | undefined
|
|
55
|
+
/** Bearer credential inferred from a value without a colon. */
|
|
56
|
+
bearerAuth?: string | undefined
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -246,6 +246,12 @@ async function measurePage(options: measurePage.Options): Promise<Measurement |
|
|
|
246
246
|
() => undefined,
|
|
247
247
|
)
|
|
248
248
|
|
|
249
|
+
// The migration scan is independent of historical quotes, so keep its TIDX
|
|
250
|
+
// round trip off the RPC critical path.
|
|
251
|
+
const migrations = recentMigrations(options).then(
|
|
252
|
+
(value) => ({ status: 'fulfilled', value }) as const,
|
|
253
|
+
(reason: unknown) => ({ reason, status: 'rejected' }) as const,
|
|
254
|
+
)
|
|
249
255
|
const rates = new Map<string, string>()
|
|
250
256
|
await Promise.all(
|
|
251
257
|
options.vaults.map(async (vault) => {
|
|
@@ -263,13 +269,13 @@ async function measurePage(options: measurePage.Options): Promise<Measurement |
|
|
|
263
269
|
}),
|
|
264
270
|
)
|
|
265
271
|
if (rates.size === 0) return { measuredAt, rates }
|
|
272
|
+
const migrationResult = await migrations
|
|
266
273
|
|
|
267
274
|
// A migration inside the window bridges two different yield engines, so the
|
|
268
|
-
// window resets instead of reporting growth across the swap.
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
for (const { blockNumber, vault } of migrations)
|
|
275
|
+
// window resets instead of reporting growth across the swap.
|
|
276
|
+
if (migrationResult.status === 'rejected') throw migrationResult.reason
|
|
277
|
+
if (!migrationResult.value) return undefined
|
|
278
|
+
for (const { blockNumber, vault } of migrationResult.value)
|
|
273
279
|
if (blockNumber > start.number && blockNumber <= end.number) rates.delete(vault)
|
|
274
280
|
return { measuredAt, rates }
|
|
275
281
|
}
|
|
@@ -306,9 +312,9 @@ async function boundaryBlocks(options: boundaryBlocks.Options): Promise<Boundari
|
|
|
306
312
|
const result = await options.getTidx(chainId).fetch({
|
|
307
313
|
chainId,
|
|
308
314
|
query: `
|
|
309
|
-
(SELECT num, timestamp FROM blocks WHERE timestamp <= '${endInstant}' ORDER BY num DESC LIMIT 1)
|
|
315
|
+
(SELECT num, timestamp FROM blocks WHERE timestamp <= '${endInstant}' ORDER BY timestamp DESC, num DESC LIMIT 1)
|
|
310
316
|
UNION ALL
|
|
311
|
-
(SELECT num, timestamp FROM blocks WHERE timestamp <= '${startInstant}' ORDER BY num DESC LIMIT 1)
|
|
317
|
+
(SELECT num, timestamp FROM blocks WHERE timestamp <= '${startInstant}' ORDER BY timestamp DESC, num DESC LIMIT 1)
|
|
312
318
|
` as string,
|
|
313
319
|
})
|
|
314
320
|
const rows = result.rows.flatMap((row) => {
|
package/src/internal/Log.test.ts
CHANGED
|
@@ -51,10 +51,11 @@ describe('middleware', () => {
|
|
|
51
51
|
`)
|
|
52
52
|
})
|
|
53
53
|
|
|
54
|
-
test('includes
|
|
54
|
+
test('includes failed optional funding counts', async () => {
|
|
55
55
|
const entries: Log.Entry[] = []
|
|
56
56
|
const api = app({ emit: (entry) => void entries.push(entry) })
|
|
57
57
|
api.get('/v1/funding/transfers', (c) => {
|
|
58
|
+
c.set('fundingDepositCountFailed', true)
|
|
58
59
|
c.set('fundingTransferCountFailed', true)
|
|
59
60
|
return c.json({ data: [], nextCursor: null }, 200)
|
|
60
61
|
})
|
|
@@ -63,6 +64,7 @@ describe('middleware', () => {
|
|
|
63
64
|
|
|
64
65
|
expect(stable(entries[0]!)).toMatchInlineSnapshot(`
|
|
65
66
|
{
|
|
67
|
+
"fundingDepositCountFailed": true,
|
|
66
68
|
"fundingTransferCountFailed": true,
|
|
67
69
|
"level": "info",
|
|
68
70
|
"method": "GET",
|
package/src/internal/Log.ts
CHANGED
|
@@ -10,10 +10,14 @@ import type * as Timing from './Timing.js'
|
|
|
10
10
|
|
|
11
11
|
/** Hono variables read by the request logger and written by other layers. */
|
|
12
12
|
export type Variables = {
|
|
13
|
+
/** Tempo chain selected for the request. */
|
|
14
|
+
chainId?: number | undefined
|
|
13
15
|
/** Whether the original request exception was already reported. */
|
|
14
16
|
errorCaptured?: boolean | undefined
|
|
15
17
|
/** Stable error code recorded by `Response.error` for the request log entry. */
|
|
16
18
|
errorCode?: string | undefined
|
|
19
|
+
/** Whether the optional funding deposit count query failed. */
|
|
20
|
+
fundingDepositCountFailed?: true | undefined
|
|
17
21
|
/** Bounded MPP relay outcome emitted by the credential routes. */
|
|
18
22
|
mpp?: Entry['mpp'] | undefined
|
|
19
23
|
/** Bounded outcomes for funding providers attempted by the request. */
|
|
@@ -125,12 +129,16 @@ export type Sponsorship = {
|
|
|
125
129
|
export type Entry = {
|
|
126
130
|
/** `'hit'` when the edge response cache served the request without reaching the origin handlers. */
|
|
127
131
|
cache?: 'hit' | undefined
|
|
132
|
+
/** Tempo chain selected for the request. */
|
|
133
|
+
chainId?: number | undefined
|
|
128
134
|
/** Total request duration in milliseconds. */
|
|
129
135
|
duration: number
|
|
130
136
|
/** Whether the original request exception was already reported. */
|
|
131
137
|
errorCaptured?: boolean | undefined
|
|
132
138
|
/** Stable error code when the response is a JSON error envelope (e.g. `not_found`, `api_key_invalid`). */
|
|
133
139
|
errorCode?: string | undefined
|
|
140
|
+
/** Whether the optional funding deposit count query failed. */
|
|
141
|
+
fundingDepositCountFailed?: true | undefined
|
|
134
142
|
/** Bounded funding provider attempts made while serving the request. */
|
|
135
143
|
fundingProviderAttempts?: readonly FundingProviderAttempt[] | undefined
|
|
136
144
|
/** Whether the optional funding transfer count query failed. */
|
|
@@ -253,7 +261,11 @@ export function middleware<environment extends Environment = Environment>(
|
|
|
253
261
|
const error = c.error as ErrorMetadata | undefined
|
|
254
262
|
const principal = c.get('principal' as never) as Auth.Principal | undefined
|
|
255
263
|
const errorCaptured = c.get('errorCaptured' as never) as boolean | undefined
|
|
264
|
+
const chainId = c.get('chainId' as never) as number | undefined
|
|
256
265
|
const errorCode = c.get('errorCode' as never) as string | undefined
|
|
266
|
+
const fundingDepositCountFailed = c.get('fundingDepositCountFailed' as never) as
|
|
267
|
+
| true
|
|
268
|
+
| undefined
|
|
257
269
|
const fundingProviderAttempts =
|
|
258
270
|
error?.fundingProviderAttempts ??
|
|
259
271
|
(c.get('fundingProviderAttempts' as never) as Entry['fundingProviderAttempts'] | undefined)
|
|
@@ -300,9 +312,11 @@ export function middleware<environment extends Environment = Environment>(
|
|
|
300
312
|
|
|
301
313
|
const result = fn({
|
|
302
314
|
...(c.get('edgeCacheStatus' as never) === 'hit' ? { cache: 'hit' as const } : {}),
|
|
315
|
+
...(chainId === undefined ? {} : { chainId }),
|
|
303
316
|
duration: round(duration),
|
|
304
317
|
...(errorCaptured === undefined ? {} : { errorCaptured }),
|
|
305
318
|
...(errorCode === undefined ? {} : { errorCode }),
|
|
319
|
+
...(fundingDepositCountFailed === undefined ? {} : { fundingDepositCountFailed }),
|
|
306
320
|
...(fundingProviderAttempts === undefined ? {} : { fundingProviderAttempts }),
|
|
307
321
|
...(fundingTransferCountFailed === undefined ? {} : { fundingTransferCountFailed }),
|
|
308
322
|
level:
|
|
@@ -11,7 +11,7 @@ type MetricEvent = {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
describe('requests', () => {
|
|
14
|
-
test('records optional funding
|
|
14
|
+
test('records optional funding count failures on successful pages', () => {
|
|
15
15
|
const events: MetricEvent[] = []
|
|
16
16
|
const metrics = Metrics.from({
|
|
17
17
|
count(name, value, tags) {
|
|
@@ -25,6 +25,7 @@ describe('requests', () => {
|
|
|
25
25
|
|
|
26
26
|
record({
|
|
27
27
|
duration: 10,
|
|
28
|
+
fundingDepositCountFailed: true,
|
|
28
29
|
fundingTransferCountFailed: true,
|
|
29
30
|
level: 'info',
|
|
30
31
|
method: 'GET',
|
|
@@ -34,6 +35,22 @@ describe('requests', () => {
|
|
|
34
35
|
status: 200,
|
|
35
36
|
})
|
|
36
37
|
|
|
38
|
+
expect(events.find((event) => event.name === 'funding_deposit_count_failure_count'))
|
|
39
|
+
.toMatchInlineSnapshot(`
|
|
40
|
+
{
|
|
41
|
+
"name": "funding_deposit_count_failure_count",
|
|
42
|
+
"tags": {
|
|
43
|
+
"cache": "miss",
|
|
44
|
+
"method": "GET",
|
|
45
|
+
"principal_environment": "none",
|
|
46
|
+
"principal_type": "unknown",
|
|
47
|
+
"route": "/v1/funding/transfers",
|
|
48
|
+
},
|
|
49
|
+
"type": "count",
|
|
50
|
+
"value": 1,
|
|
51
|
+
}
|
|
52
|
+
`)
|
|
53
|
+
|
|
37
54
|
expect(events.find((event) => event.name === 'funding_transfer_count_failure_count'))
|
|
38
55
|
.toMatchInlineSnapshot(`
|
|
39
56
|
{
|
|
@@ -637,6 +654,38 @@ describe('webhooks', () => {
|
|
|
637
654
|
`)
|
|
638
655
|
})
|
|
639
656
|
|
|
657
|
+
test('records scan pipeline blocks, backlog, and finality lag', () => {
|
|
658
|
+
const events: MetricEvent[] = []
|
|
659
|
+
const metrics = Metrics.from({
|
|
660
|
+
count(name, value, tags) {
|
|
661
|
+
events.push({ name, tags, type: 'count', value })
|
|
662
|
+
},
|
|
663
|
+
flush() {},
|
|
664
|
+
gauge(name, value, tags) {
|
|
665
|
+
events.push({ name, tags, type: 'gauge', value })
|
|
666
|
+
},
|
|
667
|
+
histogram() {},
|
|
668
|
+
})
|
|
669
|
+
const sink = MetricSink.webhooks(metrics)
|
|
670
|
+
|
|
671
|
+
sink.record({ chainId: 42431, matches: 3, origin: 'socket', outcome: 'dispatched', type: 'webhook:scan-block' }) // prettier-ignore
|
|
672
|
+
sink.record({ chainId: 42431, matches: 0, origin: 'poll', outcome: 'deferred', type: 'webhook:scan-block' }) // prettier-ignore
|
|
673
|
+
sink.record({ chainId: 42431, oldestPendingAgeMs: 5_000, pendingCount: 2, type: 'webhook:scan-backlog' }) // prettier-ignore
|
|
674
|
+
sink.record({ chainId: 42431, lagBlocks: 1, type: 'webhook:scan-finality' })
|
|
675
|
+
|
|
676
|
+
expect(events.map(({ name, tags, value }) => `${name}=${value} ${JSON.stringify(tags)}`))
|
|
677
|
+
.toMatchInlineSnapshot(`
|
|
678
|
+
[
|
|
679
|
+
"webhook_scan_block_count=1 {"chain_id":"42431","origin":"socket","outcome":"dispatched"}",
|
|
680
|
+
"webhook_scan_match_count=3 {"chain_id":"42431","origin":"socket","outcome":"dispatched"}",
|
|
681
|
+
"webhook_scan_block_count=1 {"chain_id":"42431","origin":"poll","outcome":"deferred"}",
|
|
682
|
+
"webhook_scan_pending_count=2 {"chain_id":"42431"}",
|
|
683
|
+
"webhook_scan_oldest_pending_age_ms=5000 {"chain_id":"42431"}",
|
|
684
|
+
"webhook_scan_finality_lag_blocks=1 {"chain_id":"42431"}",
|
|
685
|
+
]
|
|
686
|
+
`)
|
|
687
|
+
})
|
|
688
|
+
|
|
640
689
|
test('records obligation staging and claim outcomes', () => {
|
|
641
690
|
const events: MetricEvent[] = []
|
|
642
691
|
const metrics = Metrics.from({
|
|
@@ -159,6 +159,33 @@ export type WebhookEvent =
|
|
|
159
159
|
ok: boolean
|
|
160
160
|
type: 'webhook:enqueue'
|
|
161
161
|
}
|
|
162
|
+
| {
|
|
163
|
+
chainId: number
|
|
164
|
+
/** Rows matched when dispatched; zero otherwise. */
|
|
165
|
+
matches: number
|
|
166
|
+
/** How the head that enqueued this block was observed. */
|
|
167
|
+
origin: string
|
|
168
|
+
/** One scanned block's outcome. */
|
|
169
|
+
outcome: 'deferred' | 'dispatched' | 'empty'
|
|
170
|
+
/** Identifies one block-fanout scan. */
|
|
171
|
+
type: 'webhook:scan-block'
|
|
172
|
+
}
|
|
173
|
+
| {
|
|
174
|
+
chainId: number
|
|
175
|
+
/** Age of the oldest tracked block, absent when none are pending. */
|
|
176
|
+
oldestPendingAgeMs?: number | undefined
|
|
177
|
+
/** Tracked blocks awaiting completion reports. */
|
|
178
|
+
pendingCount: number
|
|
179
|
+
/** Identifies the coordinator's tracked-scan backlog. */
|
|
180
|
+
type: 'webhook:scan-backlog'
|
|
181
|
+
}
|
|
182
|
+
| {
|
|
183
|
+
chainId: number
|
|
184
|
+
/** Blocks between the observed head and the finalized head. */
|
|
185
|
+
lagBlocks: number
|
|
186
|
+
/** Identifies the finality gate's settling distance. */
|
|
187
|
+
type: 'webhook:scan-finality'
|
|
188
|
+
}
|
|
162
189
|
| {
|
|
163
190
|
/** Atomic ledger claim outcome. */
|
|
164
191
|
outcome: 'attempting' | 'claimed' | 'missing' | 'scheduled' | 'terminal'
|
|
@@ -275,6 +302,9 @@ export function requests(backend: Metrics): RequestSink {
|
|
|
275
302
|
backend.histogram('funding_quote_provider_duration_ms', attempt.durationMs, tags_attempt)
|
|
276
303
|
}
|
|
277
304
|
|
|
305
|
+
if (entry.fundingDepositCountFailed)
|
|
306
|
+
backend.count('funding_deposit_count_failure_count', 1, tags)
|
|
307
|
+
|
|
278
308
|
if (entry.fundingTransferCountFailed)
|
|
279
309
|
backend.count('funding_transfer_count_failure_count', 1, tags)
|
|
280
310
|
|
|
@@ -497,6 +527,28 @@ export function webhooks(backend: Metrics): WebhookSink {
|
|
|
497
527
|
})
|
|
498
528
|
return
|
|
499
529
|
}
|
|
530
|
+
case 'webhook:scan-block': {
|
|
531
|
+
const tags = {
|
|
532
|
+
chain_id: String(event.chainId),
|
|
533
|
+
origin: event.origin,
|
|
534
|
+
outcome: event.outcome,
|
|
535
|
+
}
|
|
536
|
+
backend.count('webhook_scan_block_count', 1, tags)
|
|
537
|
+
if (event.matches > 0) backend.count('webhook_scan_match_count', event.matches, tags)
|
|
538
|
+
return
|
|
539
|
+
}
|
|
540
|
+
case 'webhook:scan-backlog': {
|
|
541
|
+
const tags = { chain_id: String(event.chainId) }
|
|
542
|
+
backend.gauge('webhook_scan_pending_count', event.pendingCount, tags)
|
|
543
|
+
backend.gauge('webhook_scan_oldest_pending_age_ms', event.oldestPendingAgeMs ?? 0, tags)
|
|
544
|
+
return
|
|
545
|
+
}
|
|
546
|
+
case 'webhook:scan-finality': {
|
|
547
|
+
backend.gauge('webhook_scan_finality_lag_blocks', event.lagBlocks, {
|
|
548
|
+
chain_id: String(event.chainId),
|
|
549
|
+
})
|
|
550
|
+
return
|
|
551
|
+
}
|
|
500
552
|
case 'webhook:delivery-job-claim': {
|
|
501
553
|
backend.count('webhook_delivery_job_claim_count', 1, { outcome: event.outcome })
|
|
502
554
|
return
|