tapimo 0.10.1 → 0.11.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.
Files changed (225) hide show
  1. package/dist/App.d.ts +5249 -3572
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js.map +1 -1
  4. package/dist/Webhooks.d.ts +1 -76
  5. package/dist/Webhooks.d.ts.map +1 -1
  6. package/dist/Webhooks.js +2 -144
  7. package/dist/Webhooks.js.map +1 -1
  8. package/dist/admin/App.d.ts +74 -74
  9. package/dist/admin/apps/earn-vaults.d.ts +1 -1
  10. package/dist/admin/apps/organizations.d.ts +3 -3
  11. package/dist/admin/apps/verified-tokens.d.ts +74 -74
  12. package/dist/analytics/Analytics.d.ts.map +1 -1
  13. package/dist/analytics/Analytics.js +206 -0
  14. package/dist/analytics/Analytics.js.map +1 -1
  15. package/dist/analytics/tables/requestEvents.d.ts +4 -0
  16. package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
  17. package/dist/analytics/tables/requestEvents.js +2 -0
  18. package/dist/analytics/tables/requestEvents.js.map +1 -1
  19. package/dist/apps/data/App.d.ts +3618 -2954
  20. package/dist/apps/data/App.d.ts.map +1 -1
  21. package/dist/apps/data/App.js +7 -2
  22. package/dist/apps/data/App.js.map +1 -1
  23. package/dist/apps/data/routes/activities.d.ts +1 -1
  24. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  25. package/dist/apps/data/routes/activities.js +55 -27
  26. package/dist/apps/data/routes/activities.js.map +1 -1
  27. package/dist/apps/data/routes/balances.d.ts +11 -11
  28. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  29. package/dist/apps/data/routes/balances.js +2 -6
  30. package/dist/apps/data/routes/balances.js.map +1 -1
  31. package/dist/apps/data/routes/blocks.d.ts +5 -5
  32. package/dist/apps/data/routes/earn.d.ts +4 -4
  33. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  34. package/dist/apps/data/routes/earn.js +63 -34
  35. package/dist/apps/data/routes/earn.js.map +1 -1
  36. package/dist/apps/data/routes/exchanges.d.ts +8 -4
  37. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  38. package/dist/apps/data/routes/exchanges.js +115 -77
  39. package/dist/apps/data/routes/exchanges.js.map +1 -1
  40. package/dist/apps/data/routes/fee-amm.d.ts +26 -26
  41. package/dist/apps/data/routes/receipts.d.ts +240 -239
  42. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  43. package/dist/apps/data/routes/receipts.js +15 -14
  44. package/dist/apps/data/routes/receipts.js.map +1 -1
  45. package/dist/apps/data/routes/tokenlist.d.ts +12 -12
  46. package/dist/apps/data/routes/tokens.d.ts +4 -4
  47. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  48. package/dist/apps/data/routes/tokens.js +4 -3
  49. package/dist/apps/data/routes/tokens.js.map +1 -1
  50. package/dist/apps/data/routes/transactions.d.ts +282 -282
  51. package/dist/apps/data/routes/transfers.d.ts +13 -13
  52. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  53. package/dist/apps/data/routes/transfers.js +25 -7
  54. package/dist/apps/data/routes/transfers.js.map +1 -1
  55. package/dist/apps/data/routes/valuation.d.ts +21 -21
  56. package/dist/apps/data/routes/verified-tokens.d.ts +5 -5
  57. package/dist/apps/data/routes/webhooks.d.ts +1519 -643
  58. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  59. package/dist/apps/data/routes/webhooks.js +134 -740
  60. package/dist/apps/data/routes/webhooks.js.map +1 -1
  61. package/dist/apps/data/routes/zones.d.ts +3 -3
  62. package/dist/apps/funding/App.d.ts +233 -0
  63. package/dist/apps/funding/App.d.ts.map +1 -1
  64. package/dist/apps/funding/routes/deposit-addresses.d.ts +331 -1
  65. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
  66. package/dist/apps/funding/routes/deposit-addresses.js +207 -11
  67. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
  68. package/dist/apps/funding/routes/transfers.d.ts +33 -33
  69. package/dist/apps/management/App.d.ts +793 -16
  70. package/dist/apps/management/App.d.ts.map +1 -1
  71. package/dist/apps/management/routes/api-keys.d.ts +50 -50
  72. package/dist/apps/management/routes/faucet.d.ts +2 -2
  73. package/dist/apps/management/routes/invite-links.d.ts +42 -42
  74. package/dist/apps/management/routes/members.d.ts +2 -2
  75. package/dist/apps/management/routes/projects.d.ts +40 -40
  76. package/dist/apps/management/routes/webhooks.d.ts +1031 -59
  77. package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
  78. package/dist/apps/management/routes/webhooks.js +27 -4
  79. package/dist/apps/management/routes/webhooks.js.map +1 -1
  80. package/dist/db/Db.d.ts.map +1 -1
  81. package/dist/db/Db.js +53 -0
  82. package/dist/db/Db.js.map +1 -1
  83. package/dist/db/Schema.d.ts +7 -1
  84. package/dist/db/Schema.d.ts.map +1 -1
  85. package/dist/db/Schema.js +9 -0
  86. package/dist/db/Schema.js.map +1 -1
  87. package/dist/db/tables/fundingDepositAddresses.d.ts +20 -0
  88. package/dist/db/tables/fundingDepositAddresses.d.ts.map +1 -1
  89. package/dist/db/tables/fundingDepositAddresses.js +17 -0
  90. package/dist/db/tables/fundingDepositAddresses.js.map +1 -1
  91. package/dist/db/tables/fundingDeposits.d.ts +41 -0
  92. package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
  93. package/dist/db/tables/fundingDeposits.js +69 -0
  94. package/dist/db/tables/fundingDeposits.js.map +1 -1
  95. package/dist/db/tables/webhookQueueEvents.d.ts +3 -1
  96. package/dist/db/tables/webhookQueueEvents.d.ts.map +1 -1
  97. package/dist/db/tables/webhookQueueEvents.js +4 -0
  98. package/dist/db/tables/webhookQueueEvents.js.map +1 -1
  99. package/dist/db/tables/webhookSubscriptions.d.ts +33 -69
  100. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  101. package/dist/db/tables/webhookSubscriptions.js +63 -100
  102. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  103. package/dist/internal/Auth.d.ts +0 -26
  104. package/dist/internal/Auth.d.ts.map +1 -1
  105. package/dist/internal/Auth.js +0 -33
  106. package/dist/internal/Auth.js.map +1 -1
  107. package/dist/internal/Credential.d.ts +27 -0
  108. package/dist/internal/Credential.d.ts.map +1 -0
  109. package/dist/internal/Credential.js +34 -0
  110. package/dist/internal/Credential.js.map +1 -0
  111. package/dist/internal/EarnRates.js +11 -7
  112. package/dist/internal/EarnRates.js.map +1 -1
  113. package/dist/internal/Log.d.ts +8 -0
  114. package/dist/internal/Log.d.ts.map +1 -1
  115. package/dist/internal/Log.js +4 -0
  116. package/dist/internal/Log.js.map +1 -1
  117. package/dist/internal/MetricSink.d.ts +50 -116
  118. package/dist/internal/MetricSink.d.ts.map +1 -1
  119. package/dist/internal/MetricSink.js +60 -106
  120. package/dist/internal/MetricSink.js.map +1 -1
  121. package/dist/internal/Schema.d.ts +3 -1
  122. package/dist/internal/Schema.d.ts.map +1 -1
  123. package/dist/internal/Schema.js +4 -2
  124. package/dist/internal/Schema.js.map +1 -1
  125. package/dist/internal/Tidx.d.ts +4 -3
  126. package/dist/internal/Tidx.d.ts.map +1 -1
  127. package/dist/internal/Tidx.js +2 -2
  128. package/dist/internal/Tidx.js.map +1 -1
  129. package/dist/internal/Viem.d.ts +4 -3
  130. package/dist/internal/Viem.d.ts.map +1 -1
  131. package/dist/internal/Viem.js +3 -2
  132. package/dist/internal/Viem.js.map +1 -1
  133. package/dist/internal/WebhookTransfer.d.ts +1 -55
  134. package/dist/internal/WebhookTransfer.d.ts.map +1 -1
  135. package/dist/internal/WebhookTransfer.js +1 -239
  136. package/dist/internal/WebhookTransfer.js.map +1 -1
  137. package/dist/internal/Webhooks.d.ts +85 -44
  138. package/dist/internal/Webhooks.d.ts.map +1 -1
  139. package/dist/internal/Webhooks.js +94 -73
  140. package/dist/internal/Webhooks.js.map +1 -1
  141. package/dist/internal/funding/DepositWebhook.d.ts +25 -0
  142. package/dist/internal/funding/DepositWebhook.d.ts.map +1 -0
  143. package/dist/internal/funding/DepositWebhook.js +74 -0
  144. package/dist/internal/funding/DepositWebhook.js.map +1 -0
  145. package/dist/internal/funding/Reconciliation.d.ts +6 -0
  146. package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
  147. package/dist/internal/funding/Reconciliation.js +42 -10
  148. package/dist/internal/funding/Reconciliation.js.map +1 -1
  149. package/dist/internal/funding/index.d.ts +1 -0
  150. package/dist/internal/funding/index.d.ts.map +1 -1
  151. package/dist/internal/funding/index.js +1 -0
  152. package/dist/internal/funding/index.js.map +1 -1
  153. package/dist/internal/index.d.ts +1 -0
  154. package/dist/internal/index.d.ts.map +1 -1
  155. package/dist/internal/index.js +1 -0
  156. package/dist/internal/index.js.map +1 -1
  157. package/dist/internal/webhookDestinations/slack.d.ts.map +1 -1
  158. package/dist/internal/webhookDestinations/slack.js +16 -0
  159. package/dist/internal/webhookDestinations/slack.js.map +1 -1
  160. package/package.json +1 -1
  161. package/src/App.test.ts +30 -0
  162. package/src/App.ts +3 -0
  163. package/src/Client.test-d.ts +55 -2
  164. package/src/Webhooks.ts +2 -221
  165. package/src/analytics/Analytics.test.ts +34 -0
  166. package/src/analytics/Analytics.ts +206 -0
  167. package/src/analytics/tables/requestEvents.ts +6 -0
  168. package/src/apps/composition.test.ts +1 -0
  169. package/src/apps/data/App.ts +10 -2
  170. package/src/apps/data/routes/activities.test.ts +107 -11
  171. package/src/apps/data/routes/activities.ts +64 -32
  172. package/src/apps/data/routes/balances.ts +2 -6
  173. package/src/apps/data/routes/earn.ts +64 -34
  174. package/src/apps/data/routes/exchanges.test.ts +8 -3
  175. package/src/apps/data/routes/exchanges.ts +134 -82
  176. package/src/apps/data/routes/receipts.ts +16 -14
  177. package/src/apps/data/routes/tokens.ts +3 -2
  178. package/src/apps/data/routes/transactions.bench.ts +8 -0
  179. package/src/apps/data/routes/transfers.ts +36 -10
  180. package/src/apps/data/routes/webhooks.test.ts +179 -1497
  181. package/src/apps/data/routes/webhooks.ts +172 -975
  182. package/src/apps/funding/routes/deposit-addresses.test.ts +216 -2
  183. package/src/apps/funding/routes/deposit-addresses.ts +254 -18
  184. package/src/apps/funding/routes/transfers.test.ts +17 -2
  185. package/src/apps/management/routes/webhooks.test.ts +79 -1
  186. package/src/apps/management/routes/webhooks.ts +40 -4
  187. package/src/db/Db.test.ts +1 -5
  188. package/src/db/Db.ts +59 -0
  189. package/src/db/Schema.ts +13 -0
  190. package/src/db/tables/fundingDepositAddresses.test.ts +28 -0
  191. package/src/db/tables/fundingDepositAddresses.ts +39 -0
  192. package/src/db/tables/fundingDeposits.test.ts +82 -0
  193. package/src/db/tables/fundingDeposits.ts +130 -0
  194. package/src/db/tables/organizations.test.ts +5 -2
  195. package/src/db/tables/webhookQueueEvents.ts +7 -1
  196. package/src/db/tables/webhookSubscriptions.ts +89 -138
  197. package/src/internal/Auth.test.ts +0 -48
  198. package/src/internal/Auth.ts +0 -59
  199. package/src/internal/Credential.test.ts +51 -0
  200. package/src/internal/Credential.ts +58 -0
  201. package/src/internal/EarnRates.ts +13 -7
  202. package/src/internal/Log.test.ts +3 -1
  203. package/src/internal/Log.ts +14 -0
  204. package/src/internal/MetricSink.test.ts +75 -358
  205. package/src/internal/MetricSink.ts +117 -246
  206. package/src/internal/Schema.test.ts +18 -0
  207. package/src/internal/Schema.ts +13 -2
  208. package/src/internal/Tidx.ts +5 -4
  209. package/src/internal/Viem.test.ts +4 -0
  210. package/src/internal/Viem.ts +6 -4
  211. package/src/internal/WebhookTransfer.ts +1 -351
  212. package/src/internal/Webhooks.test.ts +111 -139
  213. package/src/internal/Webhooks.ts +181 -105
  214. package/src/internal/funding/DepositWebhook.test.ts +131 -0
  215. package/src/internal/funding/DepositWebhook.ts +101 -0
  216. package/src/internal/funding/Reconciliation.test.ts +14 -0
  217. package/src/internal/funding/Reconciliation.ts +161 -86
  218. package/src/internal/funding/index.test.ts +1 -0
  219. package/src/internal/funding/index.ts +1 -0
  220. package/src/internal/index.test.ts +1 -0
  221. package/src/internal/index.ts +1 -0
  222. package/src/internal/webhookDestinations/slack.ts +15 -0
  223. package/src/Webhooks.bench.ts +0 -714
  224. package/src/Webhooks.memory.bench.ts +0 -250
  225. package/src/Webhooks.test.ts +0 -146
@@ -1,6 +1,7 @@
1
1
  import type { ExpressionBuilder, Selectable } from 'kysely'
2
2
  import { sql } from 'kysely'
3
3
 
4
+ import * as Scope from '../../Scope.js'
4
5
  import type * as Webhooks from '../../internal/Webhooks.js'
5
6
  import type * as Db from '../Db.js'
6
7
  import type * as db_Schema from '../Schema.js'
@@ -40,6 +41,7 @@ export function toSubscription(row: Record): Webhooks.Subscription {
40
41
  ...(row.context === null ? {} : { context: row.context }),
41
42
  createdAt: row.createdAt,
42
43
  destination: row.destination,
44
+ ...(row.environment === null ? {} : { environment: row.environment }),
43
45
  eventType: row.eventType,
44
46
  ...(row.expiresAt === null ? {} : { expiresAt: row.expiresAt }),
45
47
  failureCount: row.failureCount,
@@ -50,6 +52,7 @@ export function toSubscription(row: Record): Webhooks.Subscription {
50
52
  row.ownerType === 'api_key'
51
53
  ? { orgId: row.ownerId, type: 'api_key' }
52
54
  : { payer: row.ownerId, type: 'mpp' },
55
+ ...(row.projectId === null ? {} : { projectId: row.projectId }),
53
56
  secret: row.secret,
54
57
  status: row.status,
55
58
  updatedAt: row.updatedAt,
@@ -75,6 +78,7 @@ export async function insert(
75
78
  context: subscription.context ?? null,
76
79
  createdAt: subscription.createdAt,
77
80
  destination: subscription.destination,
81
+ environment: subscription.environment ?? null,
78
82
  eventType: subscription.eventType,
79
83
  expiresAt: subscription.expiresAt ?? null,
80
84
  failureCount: subscription.failureCount,
@@ -84,6 +88,7 @@ export async function insert(
84
88
  ownerId: ownerId(subscription.owner),
85
89
  ownerType: subscription.owner.type,
86
90
  pollerCursor: options.pollerCursor ?? null,
91
+ projectId: subscription.projectId ?? null,
87
92
  secret: subscription.secret,
88
93
  status: subscription.status,
89
94
  updatedAt: subscription.updatedAt,
@@ -94,7 +99,7 @@ export async function insert(
94
99
  export declare namespace insert {
95
100
  /** Options for {@link insert}. */
96
101
  type Options = {
97
- /** Initial poller cursor committed with the subscription. */
102
+ /** Initial cursor committed with the subscription. */
98
103
  pollerCursor?: string | undefined
99
104
  }
100
105
  }
@@ -129,7 +134,7 @@ export declare namespace insertWithinLimit {
129
134
  maxPerOwner: number
130
135
  /** ISO timestamp used to exclude expired rows from the count. */
131
136
  now: string
132
- /** Initial poller cursor committed with the subscription. */
137
+ /** Initial cursor committed with the subscription. */
133
138
  pollerCursor?: string | undefined
134
139
  }
135
140
  }
@@ -149,15 +154,17 @@ export async function get(
149
154
  owner: Webhooks.Owner,
150
155
  id: string,
151
156
  now: string,
157
+ access?: Webhooks.Access,
152
158
  ): Promise<Webhooks.Subscription | null> {
153
- const row = await db.kysely
159
+ let query = db.kysely
154
160
  .selectFrom('webhook_subscriptions')
155
161
  .selectAll()
156
162
  .where('id', '=', id)
157
163
  .where('ownerType', '=', owner.type)
158
164
  .where('ownerId', '=', ownerId(owner))
159
165
  .where(live(now))
160
- .executeTakeFirst()
166
+ if (access) query = query.where(privateResourceAccess(access))
167
+ const row = await query.executeTakeFirst()
161
168
  return row ? toSubscription(row) : null
162
169
  }
163
170
 
@@ -183,6 +190,7 @@ export async function list(
183
190
  .where('ownerId', '=', ownerId(owner))
184
191
  .where(live(options.now))
185
192
  .orderBy('id', 'desc')
193
+ if (options.access) query = query.where(privateResourceAccess(options.access))
186
194
  if (options.cursor) query = query.where('id', '<', options.cursor)
187
195
  if (options.offset) query = query.offset(options.offset)
188
196
  if (options.limit !== undefined) query = query.limit(options.limit)
@@ -192,6 +200,8 @@ export async function list(
192
200
  export declare namespace list {
193
201
  /** Options for {@link list}. */
194
202
  type Options = {
203
+ /** Private-resource visibility scope. */
204
+ access?: Webhooks.Access | undefined
195
205
  /** Return subscriptions older than this id (keyset paging, newest first). */
196
206
  cursor?: string | undefined
197
207
  /** Maximum subscriptions to return. */
@@ -211,14 +221,20 @@ export declare namespace list {
211
221
  * @param now - ISO timestamp expiry is evaluated against.
212
222
  * @returns The count.
213
223
  */
214
- export async function count(db: Db.Db, owner: Webhooks.Owner, now: string): Promise<number> {
215
- const row = await db.kysely
224
+ export async function count(
225
+ db: Db.Db,
226
+ owner: Webhooks.Owner,
227
+ now: string,
228
+ access?: Webhooks.Access,
229
+ ): Promise<number> {
230
+ let query = db.kysely
216
231
  .selectFrom('webhook_subscriptions')
217
232
  .select((eb) => eb.fn.countAll().as('count'))
218
233
  .where('ownerType', '=', owner.type)
219
234
  .where('ownerId', '=', ownerId(owner))
220
235
  .where(live(now))
221
- .executeTakeFirstOrThrow()
236
+ if (access) query = query.where(privateResourceAccess(access))
237
+ const row = await query.executeTakeFirstOrThrow()
222
238
  return Number(row.count)
223
239
  }
224
240
 
@@ -275,21 +291,42 @@ export async function remove(
275
291
  owner: Webhooks.Owner,
276
292
  id: string,
277
293
  now: string,
294
+ access?: Webhooks.Access,
278
295
  ): Promise<boolean> {
279
- const result = await db.kysely
296
+ let query = db.kysely
280
297
  .deleteFrom('webhook_subscriptions')
281
298
  .where('id', '=', id)
282
299
  .where('ownerType', '=', owner.type)
283
300
  .where('ownerId', '=', ownerId(owner))
284
301
  .where(live(now))
285
- .executeTakeFirst()
302
+ if (access) query = query.where(privateResourceAccess(access))
303
+ const result = await query.executeTakeFirst()
286
304
  return result.numDeletedRows > 0n
287
305
  }
288
306
 
307
+ function privateResourceAccess(access: Webhooks.Access) {
308
+ const fundingRead = access.scopes.some(
309
+ (scope) => scope === Scope.wildcard || scope === 'funding:read',
310
+ )
311
+ return (eb: ExpressionBuilder<Db.Database, 'webhook_subscriptions'>) =>
312
+ eb.or([
313
+ eb('eventType', '!=', 'funding:deposit.updated'),
314
+ ...(fundingRead
315
+ ? [
316
+ eb.and([
317
+ eb('eventType', '=', 'funding:deposit.updated'),
318
+ eb('environment', '=', access.environment),
319
+ ...(access.projectId === undefined ? [] : [eb('projectId', '=', access.projectId)]),
320
+ ]),
321
+ ]
322
+ : []),
323
+ ])
324
+ }
325
+
289
326
  /**
290
- * Lists live `active` subscriptions for a `(chainId, eventType)` stream — the
291
- * poller's per-stream lookup, served by the partial status index. Each row
292
- * carries its poller cursor so the poller avoids a per-subscription read.
327
+ * Lists live `active` subscriptions for a `(chainId, eventType)` stream,
328
+ * served by the partial status index. Each row carries its cursor so callers
329
+ * avoid a per-subscription read.
293
330
  *
294
331
  * @param db - The database.
295
332
  * @param chainId - Chain the stream belongs to.
@@ -316,8 +353,8 @@ export async function listActive(
316
353
  }
317
354
 
318
355
  /**
319
- * Lists every live `active` subscription on a chain in one read. The poller
320
- * groups the rows by event type so empty streams cost no extra database reads.
356
+ * Lists every live `active` subscription on a chain in one read. Callers
357
+ * group the rows by event type so empty streams cost no extra database reads.
321
358
  *
322
359
  * @param db - The database.
323
360
  * @param options - Chain, configured event types, and expiry time.
@@ -342,43 +379,58 @@ export async function listActiveForChain(
342
379
  }
343
380
 
344
381
  export declare namespace listActiveForChain {
345
- /** Filters for the poller's chain-level subscription read. */
382
+ /** Filters for the chain-level subscription read. */
346
383
  type Options = {
347
384
  /** Chain whose subscriptions to read. */
348
385
  chainId: number
349
- /** Event types configured for this poller tick. */
386
+ /** Event types eligible for the read. */
350
387
  eventTypes: readonly Webhooks.EventType[]
351
388
  /** ISO timestamp expiry is evaluated against. */
352
389
  now: string
353
390
  }
354
391
  }
355
392
 
356
- /**
357
- * Reads the poller cursors of every live `active` subscription on a chain,
358
- * across event types. Feeds the cron's cursor-freshness fallback gate.
359
- *
360
- * @param db - The database.
361
- * @param chainId - Chain whose cursors to read.
362
- * @param now - ISO timestamp expiry is evaluated against.
363
- * @returns The non-null cursors, unordered.
364
- */
365
- export async function listActiveCursors(
393
+ /** Lists live funding deposit subscriptions visible to one deposit owner. */
394
+ export async function listActiveForFundingDeposit(
366
395
  db: Db.Db,
367
- chainId: number,
368
- now: string,
369
- ): Promise<string[]> {
370
- const rows = await db.kysely
396
+ options: listActiveForFundingDeposit.Options,
397
+ ): Promise<Webhooks.Subscription[]> {
398
+ let query = db.kysely
371
399
  .selectFrom('webhook_subscriptions')
372
- .select('pollerCursor')
373
- .where('chainId', '=', chainId)
400
+ .selectAll()
401
+ .where('chainId', '=', options.chainId)
402
+ .where('environment', '=', options.environment)
403
+ .where('eventType', '=', 'funding:deposit.updated')
404
+ .where('ownerId', '=', options.orgId)
405
+ .where('ownerType', '=', 'api_key')
374
406
  .where('status', '=', 'active')
375
- .where(live(now))
376
- .execute()
377
- return rows.flatMap((row) => (row.pollerCursor === null ? [] : [row.pollerCursor]))
407
+ .where(live(options.now))
408
+ const projectId = options.projectId
409
+ query =
410
+ projectId === undefined
411
+ ? query.where('projectId', 'is', null)
412
+ : query.where((eb) => eb.or([eb('projectId', 'is', null), eb('projectId', '=', projectId)]))
413
+ return (await query.orderBy('id').execute()).map(toSubscription)
414
+ }
415
+
416
+ export declare namespace listActiveForFundingDeposit {
417
+ /** Ownership scope and chain for a funding deposit event. */
418
+ type Options = {
419
+ /** Destination Tempo chain id. */
420
+ chainId: number
421
+ /** Deposit API-key environment. */
422
+ environment: 'production' | 'sandbox'
423
+ /** ISO timestamp used to exclude expired subscriptions. */
424
+ now: string
425
+ /** Owning organization id. */
426
+ orgId: string
427
+ /** Attributed deposit project, when present. */
428
+ projectId?: string | undefined
429
+ }
378
430
  }
379
431
 
380
432
  /**
381
- * Reads a subscription's poller cursor.
433
+ * Reads a subscription's keyset cursor.
382
434
  *
383
435
  * @param db - The database.
384
436
  * @param id - The subscription id (`wh_…`).
@@ -393,107 +445,6 @@ export async function getCursor(db: Db.Db, id: string): Promise<string | null> {
393
445
  return row?.pollerCursor ?? null
394
446
  }
395
447
 
396
- /**
397
- * Writes a subscription's poller cursor. Deliberately does not touch
398
- * `updatedAt` — the cursor advances every tick and is not a user mutation.
399
- *
400
- * @param db - The database.
401
- * @param id - The subscription id (`wh_…`).
402
- * @param cursor - The keyset cursor to store.
403
- */
404
- export async function setCursor(db: Db.Db, id: string, cursor: string): Promise<void> {
405
- await db.kysely
406
- .updateTable('webhook_subscriptions')
407
- .set({ pollerCursor: cursor })
408
- .where('id', '=', id)
409
- .execute()
410
- }
411
-
412
- /**
413
- * Writes a poller cursor only when the stored cursor still matches the caller's
414
- * observed value.
415
- *
416
- * @param db - The database.
417
- * @param options - Subscription, expected cursor, and next cursor.
418
- * @returns Whether the cursor was updated.
419
- */
420
- export async function compareAndSetCursor(
421
- db: Db.Db,
422
- options: compareAndSetCursor.Options,
423
- ): Promise<boolean> {
424
- let query = db.kysely
425
- .updateTable('webhook_subscriptions')
426
- .set({ pollerCursor: options.cursor })
427
- .where('id', '=', options.subscriptionId)
428
- query =
429
- options.expectedCursor === null
430
- ? query.where('pollerCursor', 'is', null)
431
- : query.where('pollerCursor', '=', options.expectedCursor)
432
- const result = await query.executeTakeFirst()
433
- return result.numUpdatedRows > 0n
434
- }
435
-
436
- export declare namespace compareAndSetCursor {
437
- /** Options for {@link compareAndSetCursor}. */
438
- type Options = {
439
- /** Next keyset cursor. */
440
- cursor: string
441
- /** Cursor observed before scanning, or null for an uninitialized subscription. */
442
- expectedCursor: string | null
443
- /** Subscription whose cursor advances. */
444
- subscriptionId: string
445
- }
446
- }
447
-
448
- /**
449
- * Writes multiple poller cursors when each stored value still matches the
450
- * caller's observed value.
451
- *
452
- * @param db - The database.
453
- * @param options - Candidate cursor writes.
454
- * @returns Subscription ids whose cursors were updated.
455
- */
456
- export async function compareAndSetCursors(
457
- db: Db.Db,
458
- options: compareAndSetCursors.Options,
459
- ): Promise<Set<string>> {
460
- if (options.cursors.length === 0) return new Set()
461
- const values = JSON.stringify(options.cursors)
462
- type Candidate = {
463
- cursor: string
464
- expectedCursor: string | null
465
- subscriptionId: string
466
- }
467
- const result = await db.kysely
468
- .with(
469
- 'candidates',
470
- () => sql<Candidate>`(
471
- SELECT
472
- candidate."subscriptionId" AS subscription_id,
473
- candidate."expectedCursor" AS expected_cursor,
474
- candidate.cursor
475
- FROM jsonb_to_recordset(${values}::jsonb)
476
- AS candidate("subscriptionId" text, "expectedCursor" text, cursor text)
477
- )`,
478
- )
479
- .updateTable('webhook_subscriptions as subscription')
480
- .from('candidates as candidate')
481
- .set((eb) => ({ pollerCursor: eb.ref('candidate.cursor') }))
482
- .whereRef('subscription.id', '=', 'candidate.subscriptionId')
483
- .whereRef('subscription.pollerCursor', 'is not distinct from', 'candidate.expectedCursor')
484
- .returning('subscription.id as subscriptionId')
485
- .execute()
486
- return new Set(result.map((row) => row.subscriptionId))
487
- }
488
-
489
- export declare namespace compareAndSetCursors {
490
- /** Options for {@link compareAndSetCursors}. */
491
- type Options = {
492
- /** Candidate cursor writes. */
493
- cursors: readonly compareAndSetCursor.Options[]
494
- }
495
- }
496
-
497
448
  /**
498
449
  * Records a successful delivery: resets the consecutive-failure counter and
499
450
  * stamps `lastDeliveryAt`.
@@ -549,7 +500,7 @@ export async function recordFailure(
549
500
 
550
501
  /**
551
502
  * Deletes expired subscriptions (their delivery rows cascade). Called
552
- * best-effort from the poller tick.
503
+ * best-effort on a schedule.
553
504
  *
554
505
  * @param db - The database.
555
506
  * @param now - ISO timestamp expiry is evaluated against.
@@ -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",