tapimo 0.10.0 → 0.10.1

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 (224) hide show
  1. package/dist/ApiKey.d.ts +2 -1
  2. package/dist/ApiKey.d.ts.map +1 -1
  3. package/dist/ApiKeys.d.ts +4 -2
  4. package/dist/ApiKeys.d.ts.map +1 -1
  5. package/dist/ApiKeys.js +26 -1
  6. package/dist/ApiKeys.js.map +1 -1
  7. package/dist/App.d.ts +3346 -2575
  8. package/dist/App.d.ts.map +1 -1
  9. package/dist/App.js +1 -0
  10. package/dist/App.js.map +1 -1
  11. package/dist/Webhooks.d.ts.map +1 -1
  12. package/dist/Webhooks.js +5 -5
  13. package/dist/Webhooks.js.map +1 -1
  14. package/dist/admin/App.d.ts +91 -84
  15. package/dist/admin/App.d.ts.map +1 -1
  16. package/dist/admin/apps/api-keys.d.ts +26 -17
  17. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  18. package/dist/admin/apps/api-keys.js +6 -0
  19. package/dist/admin/apps/api-keys.js.map +1 -1
  20. package/dist/admin/apps/organizations.d.ts +4 -2
  21. package/dist/admin/apps/organizations.d.ts.map +1 -1
  22. package/dist/admin/ui.generated.d.ts +1 -1
  23. package/dist/admin/ui.generated.js +69 -69
  24. package/dist/admin/ui.generated.js.map +1 -1
  25. package/dist/analytics/Analytics.d.ts.map +1 -1
  26. package/dist/analytics/Analytics.js +26 -0
  27. package/dist/analytics/Analytics.js.map +1 -1
  28. package/dist/apps/Zones.d.ts +4266 -82
  29. package/dist/apps/Zones.d.ts.map +1 -1
  30. package/dist/apps/Zones.js +27 -8
  31. package/dist/apps/Zones.js.map +1 -1
  32. package/dist/apps/data/App.d.ts +3467 -2696
  33. package/dist/apps/data/App.d.ts.map +1 -1
  34. package/dist/apps/data/routes/activities.d.ts +2999 -100
  35. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  36. package/dist/apps/data/routes/activities.js +340 -2
  37. package/dist/apps/data/routes/activities.js.map +1 -1
  38. package/dist/apps/data/routes/addresses.d.ts +850 -336
  39. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  40. package/dist/apps/data/routes/balances.d.ts +26 -26
  41. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  42. package/dist/apps/data/routes/balances.js +11 -3
  43. package/dist/apps/data/routes/balances.js.map +1 -1
  44. package/dist/apps/data/routes/blocks.d.ts +4 -4
  45. package/dist/apps/data/routes/coingecko.d.ts +176 -176
  46. package/dist/apps/data/routes/earn.d.ts +75 -75
  47. package/dist/apps/data/routes/earn.js +2 -2
  48. package/dist/apps/data/routes/earn.js.map +1 -1
  49. package/dist/apps/data/routes/exchanges.d.ts +120 -120
  50. package/dist/apps/data/routes/fee-amm.d.ts +24 -24
  51. package/dist/apps/data/routes/indexer.d.ts +35 -34
  52. package/dist/apps/data/routes/indexer.d.ts.map +1 -1
  53. package/dist/apps/data/routes/indexer.js +3 -2
  54. package/dist/apps/data/routes/indexer.js.map +1 -1
  55. package/dist/apps/data/routes/rpc.d.ts.map +1 -1
  56. package/dist/apps/data/routes/rpc.js +5 -1
  57. package/dist/apps/data/routes/rpc.js.map +1 -1
  58. package/dist/apps/data/routes/tokenlist.d.ts +12 -12
  59. package/dist/apps/data/routes/transactions.d.ts +281 -281
  60. package/dist/apps/data/routes/verified-tokens.d.ts +37 -37
  61. package/dist/apps/data/routes/webhooks.d.ts +418 -364
  62. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  63. package/dist/apps/data/routes/webhooks.js +161 -14
  64. package/dist/apps/data/routes/webhooks.js.map +1 -1
  65. package/dist/apps/data/routes/zones.d.ts +31 -31
  66. package/dist/apps/data/routes/zones.js +1 -1
  67. package/dist/apps/data/routes/zones.js.map +1 -1
  68. package/dist/apps/funding/App.d.ts +96 -96
  69. package/dist/apps/funding/routes/deposit-addresses.d.ts +52 -52
  70. package/dist/apps/funding/routes/deposit-addresses.js +1 -1
  71. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
  72. package/dist/apps/funding/routes/providers.d.ts +13 -13
  73. package/dist/apps/funding/routes/transfers.d.ts +32 -32
  74. package/dist/apps/funding/routes/transfers.js +1 -1
  75. package/dist/apps/funding/routes/transfers.js.map +1 -1
  76. package/dist/apps/management/App.d.ts +418 -418
  77. package/dist/apps/management/routes/api-keys.d.ts +50 -50
  78. package/dist/apps/management/routes/billing.d.ts +72 -72
  79. package/dist/apps/management/routes/billing.js +1 -1
  80. package/dist/apps/management/routes/billing.js.map +1 -1
  81. package/dist/apps/management/routes/faucet.d.ts +37 -37
  82. package/dist/apps/management/routes/faucet.js +2 -2
  83. package/dist/apps/management/routes/faucet.js.map +1 -1
  84. package/dist/apps/management/routes/invite-links.d.ts +42 -42
  85. package/dist/apps/management/routes/me.d.ts +6 -6
  86. package/dist/apps/management/routes/members.d.ts +75 -75
  87. package/dist/apps/management/routes/projects.d.ts +40 -40
  88. package/dist/apps/management/routes/usage.d.ts +76 -76
  89. package/dist/apps/management/routes/webhooks.d.ts +49 -49
  90. package/dist/apps/mcp.d.ts.map +1 -1
  91. package/dist/apps/mcp.js +4 -1
  92. package/dist/apps/mcp.js.map +1 -1
  93. package/dist/db/Db.d.ts +2 -0
  94. package/dist/db/Db.d.ts.map +1 -1
  95. package/dist/db/Db.js +58 -0
  96. package/dist/db/Db.js.map +1 -1
  97. package/dist/db/Schema.d.ts +19 -0
  98. package/dist/db/Schema.d.ts.map +1 -1
  99. package/dist/db/Schema.js +24 -0
  100. package/dist/db/Schema.js.map +1 -1
  101. package/dist/db/tables/webhookQueueEvents.d.ts +122 -0
  102. package/dist/db/tables/webhookQueueEvents.d.ts.map +1 -0
  103. package/dist/db/tables/webhookQueueEvents.js +160 -0
  104. package/dist/db/tables/webhookQueueEvents.js.map +1 -0
  105. package/dist/db/tables/webhookSubscriptions.d.ts +2 -0
  106. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  107. package/dist/db/tables/webhookSubscriptions.js +10 -0
  108. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  109. package/dist/internal/Auth.d.ts +3 -0
  110. package/dist/internal/Auth.d.ts.map +1 -1
  111. package/dist/internal/Auth.js +6 -4
  112. package/dist/internal/Auth.js.map +1 -1
  113. package/dist/internal/MetricSink.d.ts +27 -0
  114. package/dist/internal/MetricSink.d.ts.map +1 -1
  115. package/dist/internal/MetricSink.js +29 -2
  116. package/dist/internal/MetricSink.js.map +1 -1
  117. package/dist/internal/RateLimit.d.ts +4 -6
  118. package/dist/internal/RateLimit.d.ts.map +1 -1
  119. package/dist/internal/RateLimit.js +15 -14
  120. package/dist/internal/RateLimit.js.map +1 -1
  121. package/dist/internal/Store.d.ts +2 -1
  122. package/dist/internal/Store.d.ts.map +1 -1
  123. package/dist/internal/Store.js +57 -18
  124. package/dist/internal/Store.js.map +1 -1
  125. package/dist/internal/Value.d.ts +2 -0
  126. package/dist/internal/Value.d.ts.map +1 -1
  127. package/dist/internal/Value.js +25 -0
  128. package/dist/internal/Value.js.map +1 -1
  129. package/dist/internal/WebhookTransfer.d.ts +20 -0
  130. package/dist/internal/WebhookTransfer.d.ts.map +1 -1
  131. package/dist/internal/WebhookTransfer.js +7 -4
  132. package/dist/internal/WebhookTransfer.js.map +1 -1
  133. package/dist/internal/Webhooks.d.ts +139 -0
  134. package/dist/internal/Webhooks.d.ts.map +1 -1
  135. package/dist/internal/Webhooks.js +129 -7
  136. package/dist/internal/Webhooks.js.map +1 -1
  137. package/dist/internal/funding/providers/relay.d.ts +3 -4
  138. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  139. package/dist/internal/funding/providers/relay.js +59 -8
  140. package/dist/internal/funding/providers/relay.js.map +1 -1
  141. package/package.json +3 -3
  142. package/src/ApiKeys.test.ts +20 -0
  143. package/src/ApiKeys.ts +28 -1
  144. package/src/App.test.ts +10 -8
  145. package/src/App.ts +1 -0
  146. package/src/Client.test-d.ts +1 -0
  147. package/src/Webhooks.ts +19 -17
  148. package/src/admin/apps/api-keys.test.ts +9 -0
  149. package/src/admin/apps/api-keys.ts +15 -0
  150. package/src/admin/ui/src/lib/query-client.ts +3 -2
  151. package/src/admin/ui/src/routes/api-keys.tsx +31 -9
  152. package/src/admin/ui.generated.ts +69 -69
  153. package/src/analytics/Analytics.test.ts +8 -0
  154. package/src/analytics/Analytics.ts +26 -0
  155. package/src/apps/Zones.test.ts +20 -3
  156. package/src/apps/Zones.ts +28 -8
  157. package/src/apps/composition.test.ts +1 -0
  158. package/src/apps/data/App.test.ts +2 -2
  159. package/src/apps/data/routes/activities.bench.ts +41 -0
  160. package/src/apps/data/routes/activities.test.ts +103 -2
  161. package/src/apps/data/routes/activities.ts +401 -2
  162. package/src/apps/data/routes/balances.bench.ts +21 -0
  163. package/src/apps/data/routes/balances.ts +11 -3
  164. package/src/apps/data/routes/blocks.bench.ts +34 -0
  165. package/src/apps/data/routes/coingecko.bench.ts +53 -0
  166. package/src/apps/data/routes/earn.bench.ts +71 -0
  167. package/src/apps/data/routes/earn.ts +2 -2
  168. package/src/apps/data/routes/exchanges.bench.ts +85 -0
  169. package/src/apps/data/routes/fee-amm.bench.ts +13 -0
  170. package/src/apps/data/routes/indexer.bench.ts +8 -0
  171. package/src/apps/data/routes/indexer.test.ts +2 -1
  172. package/src/apps/data/routes/indexer.ts +3 -2
  173. package/src/apps/data/routes/receipts.bench.ts +39 -0
  174. package/src/apps/data/routes/rpc.bench.ts +14 -0
  175. package/src/apps/data/routes/rpc.test.ts +27 -0
  176. package/src/apps/data/routes/rpc.ts +5 -1
  177. package/src/apps/data/routes/tokenlist.bench.ts +3 -0
  178. package/src/apps/data/routes/tokens.bench.ts +76 -0
  179. package/src/apps/data/routes/transactions.bench.ts +40 -0
  180. package/src/apps/data/routes/transfers.bench.ts +28 -0
  181. package/src/apps/data/routes/valuation.bench.ts +14 -0
  182. package/src/apps/data/routes/verified-tokens.bench.ts +8 -0
  183. package/src/apps/data/routes/webhooks.bench.ts +26 -0
  184. package/src/apps/data/routes/webhooks.scan.test.ts +326 -0
  185. package/src/apps/data/routes/webhooks.test.ts +53 -6
  186. package/src/apps/data/routes/webhooks.ts +221 -14
  187. package/src/apps/data/routes/zones.bench.ts +32 -0
  188. package/src/apps/data/routes/zones.test.ts +1 -1
  189. package/src/apps/data/routes/zones.ts +1 -1
  190. package/src/apps/funding/routes/chains.test.ts +1 -1
  191. package/src/apps/funding/routes/deposit-addresses.test.ts +1 -1
  192. package/src/apps/funding/routes/deposit-addresses.ts +1 -1
  193. package/src/apps/funding/routes/providers.test.ts +1 -1
  194. package/src/apps/funding/routes/quotes.test.ts +1 -1
  195. package/src/apps/funding/routes/transfers.test.ts +3 -6
  196. package/src/apps/funding/routes/transfers.ts +1 -1
  197. package/src/apps/management/routes/billing.ts +1 -1
  198. package/src/apps/management/routes/faucet.ts +2 -2
  199. package/src/apps/management/routes/me.test.ts +31 -0
  200. package/src/apps/mcp.ts +4 -1
  201. package/src/db/Db.ts +81 -0
  202. package/src/db/Schema.ts +32 -0
  203. package/src/db/tables/webhookQueueEvents.ts +285 -0
  204. package/src/db/tables/webhookSubscriptions.ts +15 -0
  205. package/src/internal/Auth.test.ts +61 -50
  206. package/src/internal/Auth.ts +9 -4
  207. package/src/internal/EarnVaults.test.ts +2 -2
  208. package/src/internal/MetricSink.test.ts +58 -1
  209. package/src/internal/MetricSink.ts +60 -2
  210. package/src/internal/RateLimit.test.ts +23 -18
  211. package/src/internal/RateLimit.ts +23 -15
  212. package/src/internal/Store.test.ts +99 -13
  213. package/src/internal/Store.ts +52 -17
  214. package/src/internal/Tidx.test.ts +4 -4
  215. package/src/internal/Value.test.ts +25 -0
  216. package/src/internal/Value.ts +29 -0
  217. package/src/internal/WebhookTransfer.test.ts +89 -0
  218. package/src/internal/WebhookTransfer.ts +11 -7
  219. package/src/internal/Webhooks.test.ts +479 -0
  220. package/src/internal/Webhooks.ts +281 -7
  221. package/src/internal/funding/Provider.test.ts +1 -1
  222. package/src/internal/funding/providers/index.test.ts +73 -3
  223. package/src/internal/funding/providers/relay.test.ts +1 -5
  224. package/src/internal/funding/providers/relay.ts +56 -12
@@ -664,7 +664,7 @@ export namespace schema {
664
664
  .datetime()
665
665
  .check(
666
666
  z.describe('Block timestamp as an ISO 8601 string.'),
667
- z.meta({ examples: ['2026-01-14T18:38:03.000Z'] }),
667
+ z.meta({ examples: ['2026-01-14T18:38:03.685Z'] }),
668
668
  ),
669
669
  token: z.optional(
670
670
  z
@@ -790,7 +790,7 @@ export namespace schema {
790
790
  .datetime()
791
791
  .check(
792
792
  z.describe('When the block was produced, as an ISO 8601 timestamp.'),
793
- z.meta({ examples: ['2026-01-14T18:38:03.000Z'] }),
793
+ z.meta({ examples: ['2026-01-14T18:38:03.685Z'] }),
794
794
  ),
795
795
  topics: z.array(Schema.Hash).check(
796
796
  z.describe('Log topics, starting with the event signature hash (topic 0).'),
@@ -881,7 +881,7 @@ export namespace schema {
881
881
  .datetime()
882
882
  .check(
883
883
  z.describe('When the block was produced, as an ISO 8601 timestamp.'),
884
- z.meta({ examples: ['2026-01-14T18:38:03.000Z'] }),
884
+ z.meta({ examples: ['2026-01-14T18:38:03.685Z'] }),
885
885
  ),
886
886
  })
887
887
  .check(z.describe('Payload for a `block:created` event (a new block header).'))
@@ -2303,7 +2303,7 @@ export async function poll(options: poll.Options): Promise<poll.Result> {
2303
2303
  const dispatchables: poll.Dispatchable[] = []
2304
2304
  for (const item of page.items) dispatchables.push(await buildDispatchable(item))
2305
2305
  options.signal?.throwIfAborted()
2306
- const delivered = await dispatchBatch(dispatchables)
2306
+ const delivered = await dispatchBatch(dispatchables, { db })
2307
2307
  if (delivered.ok) {
2308
2308
  accepted = page.items.at(-1)?.cursor
2309
2309
  result.dispatched += dispatchables.length
@@ -2406,7 +2406,9 @@ export declare namespace poll {
2406
2406
  */
2407
2407
  dispatch?: ((input: Dispatchable) => Promise<Webhooks.Result>) | undefined
2408
2408
  /** Batch delivery transport. A successful result confirms every row in the input. */
2409
- dispatchBatch?: ((input: readonly Dispatchable[]) => Promise<Webhooks.Result>) | undefined
2409
+ dispatchBatch?:
2410
+ | ((input: readonly Dispatchable[], context: DispatchContext) => Promise<Webhooks.Result>)
2411
+ | undefined
2410
2412
  /** Event types to poll (default all). */
2411
2413
  eventTypes?: readonly EventType[] | undefined
2412
2414
  /** Current chain head; the poller emits up to `head - confirmations`. */
@@ -2426,6 +2428,12 @@ export declare namespace poll {
2426
2428
  /** Stops scanning and dispatch when the caller loses its execution lease. */
2427
2429
  signal?: AbortSignal | undefined
2428
2430
  }
2431
+
2432
+ /** Runtime context shared with a batch delivery transport. */
2433
+ type DispatchContext = {
2434
+ /** Database instance already resolved for this poll. */
2435
+ db: Db.Db
2436
+ }
2429
2437
  /** Summary counters for one poll tick. */
2430
2438
  type Result = {
2431
2439
  /** Chain that was polled. */
@@ -3124,7 +3132,12 @@ async function scanLogs(options: {
3124
3132
  if (blockNumber === undefined || logIndex === undefined) continue
3125
3133
  if (atOrBefore(blockNumber, logIndex, window.skip)) continue
3126
3134
  if (!matchesLog(log, f, event)) continue
3127
- const data = toLogEvent(log, blockIso(await cache.header(blockNumber)), f.signature, event)
3135
+ const data = toLogEvent(
3136
+ log,
3137
+ Value.blockToIso(await cache.header(blockNumber)),
3138
+ f.signature,
3139
+ event,
3140
+ )
3128
3141
  if (data) {
3129
3142
  items.push({ cursor: [data.blockNumber, data.logIndex], data })
3130
3143
  if (items.length >= batchSize) return { hasMore: true, items }
@@ -3178,12 +3191,6 @@ function matchesLog(
3178
3191
 
3179
3192
  // block:created (getBlock)
3180
3193
 
3181
- /** Converts a viem block's `timestamp` (seconds) to an ISO 8601 string. */
3182
- function blockIso(block: RpcBlock | undefined): string | undefined {
3183
- if (!block || typeof block.timestamp !== 'bigint') return undefined
3184
- return new Date(Number(block.timestamp) * 1000).toISOString()
3185
- }
3186
-
3187
3194
  type BlockEventData = {
3188
3195
  number: number
3189
3196
  hash: string
@@ -3203,7 +3210,7 @@ function toBlockEvent(block: RpcBlock): BlockEventData | undefined {
3203
3210
  const miner = Schema.Address.safeParse(block.miner)
3204
3211
  const gasUsed = typeof block.gasUsed === 'bigint' ? Number(block.gasUsed) : undefined
3205
3212
  const gasLimit = typeof block.gasLimit === 'bigint' ? Number(block.gasLimit) : undefined
3206
- const timestamp = blockIso(block)
3213
+ const timestamp = Value.blockToIso(block)
3207
3214
  const transactionCount = Array.isArray(block.transactions) ? block.transactions.length : 0
3208
3215
  if (
3209
3216
  number === undefined ||
@@ -3318,6 +3325,9 @@ async function scanTxs(options: {
3318
3325
  ? (block['transactions'] as unknown[])
3319
3326
  : []
3320
3327
  const timestamp = asString(block['timestamp'])
3328
+ // A transaction's own `blockTimestamp` is seconds-only, so the delivered
3329
+ // timestamp comes from the enclosing block, which carries milliseconds.
3330
+ const blockTime = Value.blockToIso(block)
3321
3331
  for (let offset = 0; offset < transactions.length; offset++) {
3322
3332
  const transaction = transactions[offset] as Record<string, unknown>
3323
3333
  const transactionIndex = Value.hexToNumber(transaction['transactionIndex']) ?? offset
@@ -3325,7 +3335,8 @@ async function scanTxs(options: {
3325
3335
  if (!matchesTx(transaction, f, includeCalls, timestamp, blockNumber)) continue
3326
3336
  const parsed = Transactions.schema.Rpc.Transaction.safeParse(transaction)
3327
3337
  if (!parsed.success) continue
3328
- const data = Transactions.humanizeTransaction(parsed.data)
3338
+ const humanized = Transactions.humanizeTransaction(parsed.data)
3339
+ const data = blockTime === undefined ? humanized : { ...humanized, timestamp: blockTime }
3329
3340
  items.push({ cursor: [blockNumber, transactionIndex], data })
3330
3341
  if (items.length >= batchSize) return { hasMore: true, items }
3331
3342
  }
@@ -3482,6 +3493,202 @@ function unauthorized(c: Context<App.Environment>) {
3482
3493
  })
3483
3494
  }
3484
3495
 
3496
+ // Pure per-block scanning (no RPC, no cursors, no pagination).
3497
+
3498
+ // Pure over its inputs: the caller fetches the raw block (with transactions)
3499
+ // and its logs; enrichment happens downstream. Rows require a parsable block number.
3500
+ /**
3501
+ * Matches one fetched block against a subscription set, producing the rows
3502
+ * each event type delivers. Invalid stored filters fail closed into `invalid`.
3503
+ */
3504
+ export function scanBlock<const subscription extends scanBlock.Subscription>(
3505
+ options: scanBlock.Options<subscription>,
3506
+ ): scanBlock.Result<subscription> {
3507
+ const { block, logs, subscriptions } = options
3508
+ const blockNumber = Value.hexToNumber(block['number'])
3509
+ const timestamp = Value.blockToIso(block)
3510
+ const header = toScanHeader(block)
3511
+ const viemLogs = logs.map(fromRpcLog)
3512
+ const matches: scanBlock.Match<subscription>[] = []
3513
+ const invalid: scanBlock.Invalid<subscription>[] = []
3514
+
3515
+ for (const subscription of subscriptions) {
3516
+ const filters = (() => {
3517
+ try {
3518
+ return parseFilters(subscription.eventType, subscription.filters)
3519
+ } catch (error) {
3520
+ invalid.push({ error, subscription })
3521
+ return undefined
3522
+ }
3523
+ })()
3524
+ if (filters === undefined) continue
3525
+ const matched = matches.length
3526
+ const push = (data: unknown, logIndex: number) => {
3527
+ if (blockNumber !== undefined) matches.push({ blockNumber, data, logIndex, subscription })
3528
+ }
3529
+
3530
+ // A throwing matcher invalidates only this subscription, rolling back any
3531
+ // rows it matched before the failure.
3532
+ try {
3533
+ switch (subscription.eventType) {
3534
+ case 'block:created': {
3535
+ const f = filters as z.output<typeof schema.BlockFilters>
3536
+ if (!matchesBlock(header, f)) break
3537
+ const data = toBlockEvent(header)
3538
+ if (data) push(data, 0)
3539
+ break
3540
+ }
3541
+ case 'log:emitted': {
3542
+ const f = filters as z.output<typeof schema.LogFilters>
3543
+ const event = parseEvent(f.signature)
3544
+ if (f.signature && (!event || !/^[A-Za-z_]\w*$/.test(event.name))) {
3545
+ invalid.push({
3546
+ error: new Webhooks.InvalidFilterError('log:emitted', [
3547
+ { message: 'Unsupported event signature.', path: ['signature'] },
3548
+ ]),
3549
+ subscription,
3550
+ })
3551
+ break
3552
+ }
3553
+ // Validate arg names up front so a bad filter fails closed even
3554
+ // when the block has no decodable logs.
3555
+ const unknown =
3556
+ event && f.args
3557
+ ? Object.keys(f.args).find(
3558
+ (name) => !event.inputs.some((input) => input.name === name),
3559
+ )
3560
+ : undefined
3561
+ if (unknown !== undefined) {
3562
+ invalid.push({
3563
+ error: new Webhooks.InvalidFilterError('log:emitted', [
3564
+ { message: `Unknown event argument: ${unknown}.`, path: ['args', unknown] },
3565
+ ]),
3566
+ subscription,
3567
+ })
3568
+ break
3569
+ }
3570
+ for (const log of viemLogs) {
3571
+ const decoded = event ? decodeScanLog(log, event) : log
3572
+ const logIndex = decoded.logIndex ?? undefined
3573
+ if (logIndex === undefined) continue
3574
+ if (!matchesLog(decoded, f, event)) continue
3575
+ const data = toLogEvent(decoded, timestamp, f.signature, event)
3576
+ if (data) push(data, logIndex)
3577
+ }
3578
+ break
3579
+ }
3580
+ case 'token:transfer': {
3581
+ if (timestamp === undefined) break
3582
+ const f = filters as WebhookTransfer.Filters
3583
+ for (const log of logs) {
3584
+ const data = WebhookTransfer.decode(log, timestamp)
3585
+ if (!data || !WebhookTransfer.matches(data, f)) continue
3586
+ push(data, data.logIndex)
3587
+ }
3588
+ break
3589
+ }
3590
+ case 'transaction:included': {
3591
+ const f = filters as z.output<typeof schema.TxFilters>
3592
+ const includeCalls = f.includeCalls ?? true
3593
+ const transactions = Array.isArray(block['transactions'])
3594
+ ? (block['transactions'] as unknown[])
3595
+ : []
3596
+ const rawTimestamp = asString(block['timestamp'])
3597
+ for (let offset = 0; offset < transactions.length; offset++) {
3598
+ const transaction = transactions[offset]
3599
+ if (!transaction || typeof transaction !== 'object') continue
3600
+ const tx = transaction as Record<string, unknown>
3601
+ const transactionIndex = Value.hexToNumber(tx['transactionIndex']) ?? offset
3602
+ if (!matchesTx(tx, f, includeCalls, rawTimestamp, blockNumber ?? 0)) continue
3603
+ const parsed = Transactions.schema.Rpc.Transaction.safeParse(tx)
3604
+ if (!parsed.success) continue
3605
+ const humanized = Transactions.humanizeTransaction(parsed.data)
3606
+ // A transaction's own timestamp is seconds-only; the enclosing
3607
+ // block carries milliseconds.
3608
+ push(
3609
+ timestamp === undefined ? humanized : { ...humanized, timestamp },
3610
+ transactionIndex,
3611
+ )
3612
+ }
3613
+ break
3614
+ }
3615
+ }
3616
+ } catch (error) {
3617
+ matches.length = matched
3618
+ invalid.push({ error, subscription })
3619
+ }
3620
+ }
3621
+ return { invalid, matches }
3622
+ }
3623
+
3624
+ /** Normalizes a raw RPC header to the viem-shaped fields the block matchers read. */
3625
+ function toScanHeader(block: Record<string, unknown>): RpcBlock {
3626
+ const quantity = (value: unknown) => {
3627
+ try {
3628
+ return typeof value === 'string' || typeof value === 'number' || typeof value === 'bigint'
3629
+ ? BigInt(value)
3630
+ : undefined
3631
+ } catch {
3632
+ return undefined
3633
+ }
3634
+ }
3635
+ return {
3636
+ ...block,
3637
+ gasLimit: quantity(block['gasLimit']),
3638
+ gasUsed: quantity(block['gasUsed']),
3639
+ number: quantity(block['number']),
3640
+ timestamp: quantity(block['timestamp']),
3641
+ } as RpcBlock
3642
+ }
3643
+
3644
+ export declare namespace scanBlock {
3645
+ /** The subscription fields matching consumes; extra fields flow through. */
3646
+ type Subscription = {
3647
+ /** Event type selecting the matcher and payload shape. */
3648
+ eventType: EventType
3649
+ /** Stored filter predicates, validated fail-closed per event type. */
3650
+ filters: Record<string, unknown>
3651
+ }
3652
+
3653
+ /** One fetched block plus the subscriptions to match it against. */
3654
+ type Options<subscription extends Subscription> = {
3655
+ /** Raw `eth_getBlockByNumber(_, true)` result: header plus raw transactions. */
3656
+ block: Record<string, unknown>
3657
+ /** Raw `eth_getLogs` entries for exactly this block, in log order. */
3658
+ logs: readonly RpcLog[]
3659
+ /** Active subscriptions for the chain; any mix of event types. */
3660
+ subscriptions: readonly subscription[]
3661
+ }
3662
+
3663
+ /** One deliverable row for one subscription. */
3664
+ type Match<subscription extends Subscription> = {
3665
+ /** Block the row was produced from. */
3666
+ blockNumber: number
3667
+ /** Payload the matching read endpoint returns (pre-enrichment). */
3668
+ data: unknown
3669
+ /** Row position within the block (log or transaction index; 0 for blocks). */
3670
+ logIndex: number
3671
+ /** The matched subscription, passed through verbatim. */
3672
+ subscription: subscription
3673
+ }
3674
+
3675
+ /** A subscription whose stored filters failed validation. */
3676
+ type Invalid<subscription extends Subscription> = {
3677
+ /** The validation failure. */
3678
+ error: unknown
3679
+ /** The subscription with invalid filters. */
3680
+ subscription: subscription
3681
+ }
3682
+
3683
+ /** Matches plus fail-closed filter rejections. */
3684
+ type Result<subscription extends Subscription> = {
3685
+ /** Subscriptions skipped because their stored filters failed validation. */
3686
+ invalid: readonly Invalid<subscription>[]
3687
+ /** Deliverable rows in subscription order, block order within each type. */
3688
+ matches: readonly Match<subscription>[]
3689
+ }
3690
+ }
3691
+
3485
3692
  class BlockUnavailableError extends Error {
3486
3693
  override name = 'Webhooks.BlockUnavailableError'
3487
3694
 
@@ -0,0 +1,32 @@
1
+ import * as Tidx from '../../../internal/Tidx.js'
2
+ import * as Viem from '../../../internal/Viem.js'
3
+ import * as TestApp from '../../../../test/App.js'
4
+ import * as QueryBench from '../../../../test/QueryBench.js'
5
+
6
+ const senderTag = `0x${'00'.repeat(32)}`
7
+ const zoneChainIds = {
8
+ 4217: 421_700_002,
9
+ 42431: 424_310_002,
10
+ } as const
11
+
12
+ QueryBench.bench({
13
+ chainId: false,
14
+ name: 'listZoneWithdrawals',
15
+ path: {
16
+ 4217: `/v1/zones/withdrawals/${senderTag}?chainId=${zoneChainIds[4217]}`,
17
+ 42431: `/v1/zones/withdrawals/${senderTag}?chainId=${zoneChainIds[42431]}`,
18
+ },
19
+ zones: ({ chainId }) => {
20
+ const rpcUrl = Viem.resolveUrl(process.env.RPC_URL, chainId) ?? Viem.url[chainId]
21
+ const tidxUrl = Viem.resolveUrl(process.env.TIDX_URL, chainId) ?? Tidx.url[chainId]
22
+ if (!rpcUrl || !tidxUrl) throw new Error(`Missing Zone upstream for chain ${chainId}.`)
23
+ return [
24
+ TestApp.zone({
25
+ chainId: zoneChainIds[chainId],
26
+ rpcUrl,
27
+ sourceChainId: chainId,
28
+ tidxUrl,
29
+ }),
30
+ ]
31
+ },
32
+ })
@@ -419,7 +419,7 @@ describe('GET /v1/zones/withdrawals/:senderTag', () => {
419
419
  },
420
420
  {
421
421
  "examples": [
422
- 421700001,
422
+ 1424310001,
423
423
  ],
424
424
  "name": "chainId",
425
425
  "required": true,
@@ -43,7 +43,7 @@ export namespace schema {
43
43
  .strictObject({
44
44
  chainId: Schema.ChainId.check(
45
45
  z.describe('The chain id of the Zone where the withdrawal originated.'),
46
- z.meta({ examples: [421_700_001] }),
46
+ z.meta({ examples: [1_424_310_001] }),
47
47
  ),
48
48
  })
49
49
  .check(z.describe('Query parameters used to select the originating Zone.'))
@@ -22,7 +22,7 @@ const providers = [
22
22
  integratorId: '0xdead',
23
23
  userAddress: '0x1111111111111111111111111111111111111111',
24
24
  }),
25
- Relay.relay({ userAddress: '0x1111111111111111111111111111111111111111' }),
25
+ Relay.relay({ userAddresses: TestFunding.relayQuoteUsers }),
26
26
  Rhino.rhino(),
27
27
  Squid.squid({
28
28
  integratorId: 'squid-integrator-id',
@@ -45,7 +45,7 @@ const providers = [
45
45
  integratorId: '0xdead',
46
46
  userAddress: '0x1111111111111111111111111111111111111111',
47
47
  }),
48
- Relay.relay({ userAddress: '0x1111111111111111111111111111111111111111' }),
48
+ Relay.relay({ userAddresses: TestFunding.relayQuoteUsers }),
49
49
  Rhino.rhino(),
50
50
  Squid.squid({
51
51
  integratorId: 'squid-integrator-id',
@@ -712,7 +712,7 @@ export function depositAddresses(options: depositAddresses.Options = {}) {
712
712
  .post(
713
713
  '/v1/funding/deposit-addresses/:id{fda_[A-Za-z0-9_-]+}/reconcile',
714
714
  Auth.policy({
715
- apiKey: { rateLimit: { perMinute: 10 }, scopes: ['funding:write'] },
715
+ apiKey: { rateLimit: { limit: 10, period: 'minute' }, scopes: ['funding:write'] },
716
716
  }),
717
717
  OpenApi.validate('param', schema.reconcileFundingDepositAddress.Params, {
718
718
  code: 'param_invalid',
@@ -28,7 +28,7 @@ const providers = [
28
28
  integratorId: '0xdead',
29
29
  userAddress: '0x1111111111111111111111111111111111111111',
30
30
  }),
31
- Relay.relay({ userAddress: '0x1111111111111111111111111111111111111111' }),
31
+ Relay.relay({ userAddresses: TestFunding.relayQuoteUsers }),
32
32
  Rhino.rhino(),
33
33
  Squid.squid({
34
34
  integratorId: 'squid-integrator-id',
@@ -28,7 +28,7 @@ const providers = [
28
28
  integratorId: '0xdead',
29
29
  userAddress: '0x1111111111111111111111111111111111111111',
30
30
  }),
31
- Relay.relay({ userAddress: '0x1111111111111111111111111111111111111111' }),
31
+ Relay.relay({ userAddresses: TestFunding.relayQuoteUsers }),
32
32
  Rhino.rhino(),
33
33
  Squid.squid({
34
34
  integratorId: 'squid-integrator-id',
@@ -27,11 +27,8 @@ const db = TestApp.database()
27
27
  /** Providers exercised against executable calls on a forked source chain. */
28
28
  const executableProviders = [
29
29
  {
30
- create: (sender: string) =>
31
- Relay.relay({
32
- ...(process.env.RELAY_API_KEY ? { apiKey: process.env.RELAY_API_KEY } : {}),
33
- userAddress: sender,
34
- }),
30
+ create: (_sender: string) =>
31
+ Relay.relay(process.env.RELAY_API_KEY ? { apiKey: process.env.RELAY_API_KEY } : {}),
35
32
  id: 'relay',
36
33
  },
37
34
  ] as const
@@ -282,7 +279,7 @@ describe('POST /funding/transfers', () => {
282
279
  providerResponse(),
283
280
  ) {
284
281
  return FundingProvider.from({
285
- ...Relay.relay({ userAddress: '0x1111111111111111111111111111111111111111' }),
282
+ ...Relay.relay(),
286
283
  prepareTransfer,
287
284
  })
288
285
  }
@@ -110,7 +110,7 @@ export namespace schema {
110
110
  .check(
111
111
  z.regex(/^eip155:\d+$/),
112
112
  z.describe('Configured Zone CAIP-2 id.'),
113
- z.meta({ examples: ['eip155:421700001'] }),
113
+ z.meta({ examples: ['eip155:1424310001'] }),
114
114
  ),
115
115
  destinationToken: FundingTransfer.schema.CreateRequest.shape.destinationToken,
116
116
  mode: destinationCreateFields.mode,
@@ -699,7 +699,7 @@ export function billing() {
699
699
  Auth.policy({
700
700
  apiKey: false,
701
701
  mpp: false,
702
- public: { rateLimit: { perMinute: 600 } },
702
+ public: { rateLimit: { limit: 600, period: 'minute' } },
703
703
  session: false,
704
704
  }),
705
705
  OpenApi.describeRoute({ hide: true, summary: 'Stripe webhook' }),
@@ -10,8 +10,8 @@ import * as Value from '../../../internal/Value.js'
10
10
  import * as Viem from '../../../internal/Viem.js'
11
11
  import type { Environment } from '../App.js'
12
12
 
13
- const addressRateLimit = { perMinute: 1 } satisfies RateLimit.Limit
14
- const userRateLimit = { perMinute: 3 } satisfies RateLimit.Limit
13
+ const addressRateLimit = { limit: 1, period: 'minute' } satisfies RateLimit.Limit
14
+ const userRateLimit = { limit: 3, period: 'minute' } satisfies RateLimit.Limit
15
15
 
16
16
  /** OpenAPI schemas owned by the sandbox faucet. */
17
17
  export namespace schema {
@@ -1,7 +1,10 @@
1
1
  import { serve } from '@hono/node-server'
2
2
  import * as Accounts from 'accounts/server'
3
3
  import { exportJWK, generateKeyPair } from 'jose'
4
+ import { P256 } from 'ox'
4
5
  import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
6
+ import { Account } from 'viem/tempo'
7
+ import { tempoTestnet } from 'viem/tempo/chains'
5
8
 
6
9
  import * as TestApp from '../../../../test/App.js'
7
10
  import * as Me from './me.js'
@@ -18,6 +21,34 @@ function createApp() {
18
21
  })
19
22
  }
20
23
 
24
+ describe('SIWE verification', () => {
25
+ test('surfaces configured RPC failures as dependency errors', async () => {
26
+ const account = Account.fromHeadlessWebAuthn(P256.randomPrivateKey(), {
27
+ origin,
28
+ rpId: 'localhost',
29
+ })
30
+ let chainId_seen: number | undefined
31
+ const app = TestApp.create({
32
+ rpc: ({ chainId }) => {
33
+ chainId_seen = chainId
34
+ return { url: 'http://127.0.0.1:1' }
35
+ },
36
+ })
37
+
38
+ const { verifyResponse } = await TestApp.signIn(app, account, {
39
+ chainId: tempoTestnet.id,
40
+ })
41
+
42
+ expect(verifyResponse.status).toBe(502)
43
+ expect(chainId_seen).toMatchInlineSnapshot(`42431`)
44
+ expect(await verifyResponse.json()).toMatchInlineSnapshot(`
45
+ {
46
+ "error": "signature verification unavailable",
47
+ }
48
+ `)
49
+ })
50
+ })
51
+
21
52
  describe('GET /me', () => {
22
53
  test('returns the signed-in user', async () => {
23
54
  const app = createApp()
package/src/apps/mcp.ts CHANGED
@@ -138,7 +138,10 @@ export function mcp(options: mcp.Options) {
138
138
  const app = new Hono<App.Environment>()
139
139
  .post(
140
140
  '/mcp',
141
- Auth.policy({ apiKey: { scopes: [] }, public: { rateLimit: { perMinute: 100 } } }),
141
+ Auth.policy({
142
+ apiKey: { scopes: [] },
143
+ public: { rateLimit: { limit: 100, period: 'minute' } },
144
+ }),
142
145
  OpenApi.describeRoute({
143
146
  description:
144
147
  "A stateless streamable-HTTP MCP server: send JSON-RPC 2.0 messages as `POST` with `Accept: application/json, text/event-stream`. Tools mirror the API's data domain, plus `docs_*` documentation search. Anonymous requests allow 100 requests per minute; send `Authorization: Bearer <api key>` for protected tools.",
package/src/db/Db.ts CHANGED
@@ -36,6 +36,7 @@ import type * as Users from './tables/users.js'
36
36
  import type * as VerifiedTokenRequests from './tables/verifiedTokenRequests.js'
37
37
  import type * as VerifiedTokens from './tables/verifiedTokens.js'
38
38
  import type * as WebhookDeliveries from './tables/webhookDeliveries.js'
39
+ import type * as WebhookQueueEvents from './tables/webhookQueueEvents.js'
39
40
  import type * as WebhookSubscriptions from './tables/webhookSubscriptions.js'
40
41
 
41
42
  /**
@@ -82,6 +83,7 @@ export type Database = {
82
83
  verified_token_requests: VerifiedTokenRequests.Table
83
84
  verified_tokens: VerifiedTokens.Table
84
85
  webhook_deliveries: WebhookDeliveries.Table
86
+ webhook_queue_events: WebhookQueueEvents.Table
85
87
  webhook_subscriptions: WebhookSubscriptions.Table
86
88
  }
87
89
 
@@ -1281,6 +1283,85 @@ export const migrations: readonly Migration[] = [
1281
1283
  .on('fundingDepositAddresses')
1282
1284
  .columns(['orgId', 'environment', 'address', 'projectId']),
1283
1285
  },
1286
+ {
1287
+ name: '0115_webhook_queue_events',
1288
+ up: (db) =>
1289
+ db.schema
1290
+ .createTable('webhookQueueEvents')
1291
+ .ifNotExists()
1292
+ .addColumn('subscriptionId', 'text', (c) =>
1293
+ c.notNull().references('webhookSubscriptions.id').onDelete('cascade'),
1294
+ )
1295
+ .addColumn('eventId', 'text', (c) => c.notNull())
1296
+ .addColumn('envelope', 'jsonb', (c) => c.notNull())
1297
+ .addColumn('createdAt', 'text', (c) => c.notNull())
1298
+ .addColumn('expiresAt', 'text', (c) => c.notNull())
1299
+ .addPrimaryKeyConstraint('webhook_queue_events_pkey', ['subscriptionId', 'eventId']),
1300
+ },
1301
+ {
1302
+ name: '0116_webhook_queue_events_expires_index',
1303
+ up: (db) =>
1304
+ db.schema
1305
+ .createIndex('webhook_queue_events_expires_idx')
1306
+ .ifNotExists()
1307
+ .on('webhookQueueEvents')
1308
+ .column('expiresAt'),
1309
+ },
1310
+ {
1311
+ name: '0117_webhook_queue_events_ledger_columns',
1312
+ up: (db) =>
1313
+ raw(
1314
+ db,
1315
+ sql`ALTER TABLE webhook_queue_events
1316
+ ADD COLUMN IF NOT EXISTS status text NOT NULL DEFAULT 'pending',
1317
+ ADD COLUMN IF NOT EXISTS attempt_count integer NOT NULL DEFAULT 0,
1318
+ ADD COLUMN IF NOT EXISTS attempting_at text,
1319
+ ADD COLUMN IF NOT EXISTS next_attempt_at text`,
1320
+ ),
1321
+ },
1322
+ {
1323
+ name: '0118_webhook_queue_events_status_check',
1324
+ up: (db) =>
1325
+ raw(
1326
+ db,
1327
+ sql`ALTER TABLE webhook_queue_events DROP CONSTRAINT IF EXISTS webhook_queue_events_status_check, ADD CONSTRAINT webhook_queue_events_status_check CHECK (status IN ('failed', 'pending', 'skipped', 'succeeded'))`,
1328
+ ),
1329
+ },
1330
+ {
1331
+ name: '0119_webhook_queue_events_pending_index',
1332
+ up: (db) =>
1333
+ // NULLS FIRST matches the due-sweep order (null = due immediately);
1334
+ // INCLUDE lets the pending gauge take min(created_at) index-only.
1335
+ raw(
1336
+ db,
1337
+ sql`CREATE INDEX IF NOT EXISTS webhook_queue_events_pending_idx ON webhook_queue_events (next_attempt_at NULLS FIRST) INCLUDE (created_at) WHERE status = 'pending'`,
1338
+ ),
1339
+ },
1340
+ {
1341
+ name: '0120_webhook_queue_events_pending_index_drop',
1342
+ up: (db) => raw(db, sql`DROP INDEX IF EXISTS webhook_queue_events_pending_idx`),
1343
+ },
1344
+ {
1345
+ name: '0121_webhook_queue_events_pending_index_expiry',
1346
+ up: (db) =>
1347
+ // Recreated with expires_at so claimable and gauge queries, which all
1348
+ // filter expiry at query time, stay index-only.
1349
+ raw(
1350
+ db,
1351
+ sql`CREATE INDEX IF NOT EXISTS webhook_queue_events_pending_idx ON webhook_queue_events (next_attempt_at NULLS FIRST) INCLUDE (created_at, expires_at) WHERE status = 'pending'`,
1352
+ ),
1353
+ },
1354
+ {
1355
+ name: '0122_webhook_queue_events_expired_pending_cleanup',
1356
+ up: (db) =>
1357
+ // Pre-ledger rows already past their staging TTL were dead under the
1358
+ // old read path; pending rows no longer expire, so drop them once
1359
+ // instead of resurrecting them as eternal obligations.
1360
+ raw(
1361
+ db,
1362
+ sql`DELETE FROM webhook_queue_events WHERE status = 'pending' AND attempting_at IS NULL AND expires_at <= to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')`,
1363
+ ),
1364
+ },
1284
1365
  ]
1285
1366
 
1286
1367
  /**
package/src/db/Schema.ts CHANGED
@@ -874,3 +874,35 @@ export const WebhookDelivery = z.object({
874
874
 
875
875
  /** Columns of the `webhook_deliveries` table. */
876
876
  export type WebhookDelivery = z.output<typeof WebhookDelivery>
877
+
878
+ /** A durable webhook envelope staged before its compact Queue reference is admitted. */
879
+ export const WebhookQueueEvent = z.object({
880
+ attemptCount: z.number().check(z.describe('Delivery attempts claimed so far.')),
881
+ attemptingAt: z
882
+ .nullable(z.string())
883
+ .check(z.describe('ISO timestamp of the current claim, or null when unclaimed.')),
884
+ createdAt: z.string().check(z.describe('ISO timestamp when the envelope was staged.')),
885
+ envelope: z
886
+ .custom<Webhooks.Envelope>()
887
+ .check(z.describe('The full webhook envelope retained outside Cloudflare Queues.')),
888
+ eventId: z.string().check(z.describe('Idempotent event id (`evt_...`).')),
889
+ expiresAt: z
890
+ .string()
891
+ .check(
892
+ z.describe(
893
+ 'ISO dedupe retention deadline, restarted at terminal transitions; inert while pending.',
894
+ ),
895
+ ),
896
+ nextAttemptAt: z
897
+ .nullable(z.string())
898
+ .check(z.describe('ISO retry due time, or null when due immediately.')),
899
+ status: z
900
+ .enum(['failed', 'pending', 'skipped', 'succeeded'])
901
+ .check(z.describe('Delivery-obligation lifecycle status.')),
902
+ subscriptionId: z
903
+ .string()
904
+ .check(z.describe('Owning subscription id (`wh_...`); FK with `ON DELETE CASCADE`.')),
905
+ })
906
+
907
+ /** Columns of the `webhook_queue_events` table. */
908
+ export type WebhookQueueEvent = z.output<typeof WebhookQueueEvent>