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.
Files changed (144) hide show
  1. package/dist/App.d.ts +1776 -1543
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/Webhooks.d.ts +1 -1
  4. package/dist/Webhooks.d.ts.map +1 -1
  5. package/dist/Webhooks.js +1 -1
  6. package/dist/Webhooks.js.map +1 -1
  7. package/dist/admin/apps/api-keys.d.ts +4 -4
  8. package/dist/analytics/Analytics.d.ts.map +1 -1
  9. package/dist/analytics/Analytics.js +206 -0
  10. package/dist/analytics/Analytics.js.map +1 -1
  11. package/dist/analytics/tables/requestEvents.d.ts +4 -0
  12. package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
  13. package/dist/analytics/tables/requestEvents.js +2 -0
  14. package/dist/analytics/tables/requestEvents.js.map +1 -1
  15. package/dist/apps/data/App.d.ts.map +1 -1
  16. package/dist/apps/data/App.js +2 -0
  17. package/dist/apps/data/App.js.map +1 -1
  18. package/dist/apps/data/routes/addresses.d.ts +340 -340
  19. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  20. package/dist/apps/data/routes/balances.js +2 -6
  21. package/dist/apps/data/routes/balances.js.map +1 -1
  22. package/dist/apps/data/routes/coingecko.d.ts +176 -176
  23. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  24. package/dist/apps/data/routes/earn.js +63 -34
  25. package/dist/apps/data/routes/earn.js.map +1 -1
  26. package/dist/apps/data/routes/exchanges.d.ts +4 -0
  27. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  28. package/dist/apps/data/routes/exchanges.js +115 -77
  29. package/dist/apps/data/routes/exchanges.js.map +1 -1
  30. package/dist/apps/data/routes/indexer.d.ts +34 -34
  31. package/dist/apps/data/routes/receipts.d.ts +4 -3
  32. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  33. package/dist/apps/data/routes/receipts.js +15 -14
  34. package/dist/apps/data/routes/receipts.js.map +1 -1
  35. package/dist/apps/data/routes/tokens.d.ts +1 -1
  36. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  37. package/dist/apps/data/routes/tokens.js +4 -3
  38. package/dist/apps/data/routes/tokens.js.map +1 -1
  39. package/dist/apps/data/routes/transactions.d.ts +281 -281
  40. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  41. package/dist/apps/data/routes/transfers.js +25 -7
  42. package/dist/apps/data/routes/transfers.js.map +1 -1
  43. package/dist/apps/data/routes/webhooks.d.ts +380 -364
  44. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  45. package/dist/apps/data/routes/webhooks.js +16 -9
  46. package/dist/apps/data/routes/webhooks.js.map +1 -1
  47. package/dist/apps/data/routes/zones.d.ts +31 -31
  48. package/dist/apps/funding/App.d.ts +233 -0
  49. package/dist/apps/funding/App.d.ts.map +1 -1
  50. package/dist/apps/funding/routes/deposit-addresses.d.ts +330 -0
  51. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
  52. package/dist/apps/funding/routes/deposit-addresses.js +207 -11
  53. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
  54. package/dist/apps/funding/routes/providers.d.ts +13 -13
  55. package/dist/apps/management/routes/faucet.d.ts +32 -32
  56. package/dist/apps/management/routes/members.d.ts +79 -79
  57. package/dist/apps/management/routes/usage.d.ts +80 -80
  58. package/dist/db/Db.d.ts.map +1 -1
  59. package/dist/db/Db.js +32 -0
  60. package/dist/db/Db.js.map +1 -1
  61. package/dist/db/tables/fundingDepositAddresses.d.ts +20 -0
  62. package/dist/db/tables/fundingDepositAddresses.d.ts.map +1 -1
  63. package/dist/db/tables/fundingDepositAddresses.js +17 -0
  64. package/dist/db/tables/fundingDepositAddresses.js.map +1 -1
  65. package/dist/db/tables/fundingDeposits.d.ts +41 -0
  66. package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
  67. package/dist/db/tables/fundingDeposits.js +69 -0
  68. package/dist/db/tables/fundingDeposits.js.map +1 -1
  69. package/dist/internal/Auth.d.ts +0 -26
  70. package/dist/internal/Auth.d.ts.map +1 -1
  71. package/dist/internal/Auth.js +0 -33
  72. package/dist/internal/Auth.js.map +1 -1
  73. package/dist/internal/Credential.d.ts +27 -0
  74. package/dist/internal/Credential.d.ts.map +1 -0
  75. package/dist/internal/Credential.js +34 -0
  76. package/dist/internal/Credential.js.map +1 -0
  77. package/dist/internal/EarnRates.js +11 -7
  78. package/dist/internal/EarnRates.js.map +1 -1
  79. package/dist/internal/Log.d.ts +8 -0
  80. package/dist/internal/Log.d.ts.map +1 -1
  81. package/dist/internal/Log.js +4 -0
  82. package/dist/internal/Log.js.map +1 -1
  83. package/dist/internal/MetricSink.d.ts +24 -0
  84. package/dist/internal/MetricSink.d.ts.map +1 -1
  85. package/dist/internal/MetricSink.js +25 -0
  86. package/dist/internal/MetricSink.js.map +1 -1
  87. package/dist/internal/Tidx.d.ts +4 -3
  88. package/dist/internal/Tidx.d.ts.map +1 -1
  89. package/dist/internal/Tidx.js +2 -2
  90. package/dist/internal/Tidx.js.map +1 -1
  91. package/dist/internal/Viem.d.ts +4 -3
  92. package/dist/internal/Viem.d.ts.map +1 -1
  93. package/dist/internal/Viem.js +3 -2
  94. package/dist/internal/Viem.js.map +1 -1
  95. package/dist/internal/Webhooks.d.ts +2 -0
  96. package/dist/internal/Webhooks.d.ts.map +1 -1
  97. package/dist/internal/Webhooks.js +10 -3
  98. package/dist/internal/Webhooks.js.map +1 -1
  99. package/dist/internal/index.d.ts +1 -0
  100. package/dist/internal/index.d.ts.map +1 -1
  101. package/dist/internal/index.js +1 -0
  102. package/dist/internal/index.js.map +1 -1
  103. package/package.json +1 -1
  104. package/src/App.test.ts +30 -0
  105. package/src/Client.test-d.ts +33 -1
  106. package/src/Webhooks.ts +1 -1
  107. package/src/analytics/Analytics.test.ts +34 -0
  108. package/src/analytics/Analytics.ts +206 -0
  109. package/src/analytics/tables/requestEvents.ts +6 -0
  110. package/src/apps/composition.test.ts +1 -0
  111. package/src/apps/data/App.ts +2 -0
  112. package/src/apps/data/routes/balances.ts +2 -6
  113. package/src/apps/data/routes/earn.ts +64 -34
  114. package/src/apps/data/routes/exchanges.test.ts +8 -3
  115. package/src/apps/data/routes/exchanges.ts +134 -82
  116. package/src/apps/data/routes/receipts.ts +16 -14
  117. package/src/apps/data/routes/tokens.ts +3 -2
  118. package/src/apps/data/routes/transactions.bench.ts +8 -0
  119. package/src/apps/data/routes/transfers.ts +36 -10
  120. package/src/apps/data/routes/webhooks.ts +31 -10
  121. package/src/apps/funding/routes/deposit-addresses.test.ts +216 -2
  122. package/src/apps/funding/routes/deposit-addresses.ts +254 -18
  123. package/src/apps/funding/routes/transfers.test.ts +17 -2
  124. package/src/db/Db.ts +36 -0
  125. package/src/db/tables/fundingDepositAddresses.test.ts +28 -0
  126. package/src/db/tables/fundingDepositAddresses.ts +39 -0
  127. package/src/db/tables/fundingDeposits.test.ts +82 -0
  128. package/src/db/tables/fundingDeposits.ts +130 -0
  129. package/src/internal/Auth.test.ts +0 -48
  130. package/src/internal/Auth.ts +0 -59
  131. package/src/internal/Credential.test.ts +51 -0
  132. package/src/internal/Credential.ts +58 -0
  133. package/src/internal/EarnRates.ts +13 -7
  134. package/src/internal/Log.test.ts +3 -1
  135. package/src/internal/Log.ts +14 -0
  136. package/src/internal/MetricSink.test.ts +50 -1
  137. package/src/internal/MetricSink.ts +52 -0
  138. package/src/internal/Tidx.ts +5 -4
  139. package/src/internal/Viem.test.ts +4 -0
  140. package/src/internal/Viem.ts +6 -4
  141. package/src/internal/Webhooks.test.ts +9 -0
  142. package/src/internal/Webhooks.ts +10 -3
  143. package/src/internal/index.test.ts +1 -0
  144. 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 }[]
@@ -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. Scanned only
269
- // once a rate exists for it to invalidate.
270
- const migrations = await recentMigrations(options)
271
- if (!migrations) return undefined
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) => {
@@ -51,10 +51,11 @@ describe('middleware', () => {
51
51
  `)
52
52
  })
53
53
 
54
- test('includes a failed optional funding transfer count', async () => {
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",
@@ -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 transfer count failures on successful pages', () => {
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