tapimo 0.8.0 → 0.10.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 (223) hide show
  1. package/dist/App.d.ts +7410 -7613
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +0 -1
  4. package/dist/App.js.map +1 -1
  5. package/dist/Webhooks.d.ts +21 -20
  6. package/dist/Webhooks.d.ts.map +1 -1
  7. package/dist/Webhooks.js +66 -23
  8. package/dist/Webhooks.js.map +1 -1
  9. package/dist/admin/apps/verified-tokens.d.ts +74 -74
  10. package/dist/apps/Zones.d.ts +34 -22
  11. package/dist/apps/Zones.d.ts.map +1 -1
  12. package/dist/apps/Zones.js +9 -8
  13. package/dist/apps/Zones.js.map +1 -1
  14. package/dist/apps/data/App.d.ts +3120 -2782
  15. package/dist/apps/data/App.d.ts.map +1 -1
  16. package/dist/apps/data/FxOracle.d.ts +4 -5
  17. package/dist/apps/data/FxOracle.d.ts.map +1 -1
  18. package/dist/apps/data/FxOracle.js +19 -4
  19. package/dist/apps/data/FxOracle.js.map +1 -1
  20. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  21. package/dist/apps/data/routes/activities.js +32 -27
  22. package/dist/apps/data/routes/activities.js.map +1 -1
  23. package/dist/apps/data/routes/addresses.d.ts +283 -52
  24. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  25. package/dist/apps/data/routes/balances.d.ts +291 -21
  26. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  27. package/dist/apps/data/routes/balances.js +232 -66
  28. package/dist/apps/data/routes/balances.js.map +1 -1
  29. package/dist/apps/data/routes/blocks.d.ts +4 -4
  30. package/dist/apps/data/routes/coingecko.d.ts +54 -54
  31. package/dist/apps/data/routes/earn.d.ts +642 -126
  32. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  33. package/dist/apps/data/routes/earn.js +627 -58
  34. package/dist/apps/data/routes/earn.js.map +1 -1
  35. package/dist/apps/data/routes/exchanges.d.ts +120 -120
  36. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  37. package/dist/apps/data/routes/exchanges.js +114 -118
  38. package/dist/apps/data/routes/exchanges.js.map +1 -1
  39. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  40. package/dist/apps/data/routes/receipts.js +68 -47
  41. package/dist/apps/data/routes/receipts.js.map +1 -1
  42. package/dist/apps/data/routes/tokens.d.ts +2 -0
  43. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  44. package/dist/apps/data/routes/transactions.d.ts +59 -51
  45. package/dist/apps/data/routes/transactions.d.ts.map +1 -1
  46. package/dist/apps/data/routes/transactions.js +25 -23
  47. package/dist/apps/data/routes/transactions.js.map +1 -1
  48. package/dist/apps/data/routes/transfers.d.ts +40 -18
  49. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  50. package/dist/apps/data/routes/transfers.js +618 -105
  51. package/dist/apps/data/routes/transfers.js.map +1 -1
  52. package/dist/apps/data/routes/valuation.d.ts +21 -21
  53. package/dist/apps/data/routes/valuation.d.ts.map +1 -1
  54. package/dist/apps/data/routes/valuation.js +2 -2
  55. package/dist/apps/data/routes/valuation.js.map +1 -1
  56. package/dist/apps/data/routes/verified-tokens.d.ts +37 -37
  57. package/dist/apps/data/routes/webhooks.d.ts +102 -505
  58. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  59. package/dist/apps/data/routes/webhooks.js +484 -342
  60. package/dist/apps/data/routes/webhooks.js.map +1 -1
  61. package/dist/apps/data/routes/zones.d.ts +12 -12
  62. package/dist/apps/data/routes/zones.d.ts.map +1 -1
  63. package/dist/apps/data/routes/zones.js +3 -1
  64. package/dist/apps/data/routes/zones.js.map +1 -1
  65. package/dist/apps/funding/App.d.ts +127 -386
  66. package/dist/apps/funding/App.d.ts.map +1 -1
  67. package/dist/apps/funding/App.js +20 -5
  68. package/dist/apps/funding/App.js.map +1 -1
  69. package/dist/apps/funding/routes/deposit-addresses.d.ts +141 -532
  70. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
  71. package/dist/apps/funding/routes/deposit-addresses.js +21 -103
  72. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
  73. package/dist/apps/funding/routes/transfers.d.ts +4 -4
  74. package/dist/apps/funding/routes/transfers.js +5 -5
  75. package/dist/apps/funding/routes/transfers.js.map +1 -1
  76. package/dist/apps/management/App.d.ts +429 -727
  77. package/dist/apps/management/App.d.ts.map +1 -1
  78. package/dist/apps/management/routes/api-keys.d.ts +24 -24
  79. package/dist/apps/management/routes/billing.d.ts +72 -72
  80. package/dist/apps/management/routes/faucet.d.ts +6 -6
  81. package/dist/apps/management/routes/invite-links.d.ts +38 -38
  82. package/dist/apps/management/routes/me.d.ts +6 -6
  83. package/dist/apps/management/routes/members.d.ts +30 -30
  84. package/dist/apps/management/routes/projects.d.ts +20 -20
  85. package/dist/apps/management/routes/usage.d.ts +24 -24
  86. package/dist/apps/management/routes/webhooks.d.ts +158 -573
  87. package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
  88. package/dist/apps/mpp/App.d.ts +12 -0
  89. package/dist/apps/mpp/App.d.ts.map +1 -1
  90. package/dist/apps/mpp/Mpp.d.ts +3 -0
  91. package/dist/apps/mpp/Mpp.d.ts.map +1 -1
  92. package/dist/apps/mpp/Mpp.js +3 -0
  93. package/dist/apps/mpp/Mpp.js.map +1 -1
  94. package/dist/apps/mpp/internal/Adapter.d.ts.map +1 -1
  95. package/dist/apps/mpp/internal/Adapter.js +9 -2
  96. package/dist/apps/mpp/internal/Adapter.js.map +1 -1
  97. package/dist/db/Db.d.ts +2 -2
  98. package/dist/db/Db.d.ts.map +1 -1
  99. package/dist/db/Db.js +25 -15
  100. package/dist/db/Db.js.map +1 -1
  101. package/dist/db/tables/fundingDeposits.d.ts +18 -1
  102. package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
  103. package/dist/db/tables/fundingDeposits.js +46 -2
  104. package/dist/db/tables/fundingDeposits.js.map +1 -1
  105. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  106. package/dist/db/tables/sponsoredTransactions.js +4 -1
  107. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  108. package/dist/db/tables/webhookSubscriptions.d.ts +56 -3
  109. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  110. package/dist/db/tables/webhookSubscriptions.js +76 -3
  111. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  112. package/dist/internal/Auth.d.ts +4 -0
  113. package/dist/internal/Auth.d.ts.map +1 -1
  114. package/dist/internal/Auth.js +11 -2
  115. package/dist/internal/Auth.js.map +1 -1
  116. package/dist/internal/MetricSink.d.ts +112 -3
  117. package/dist/internal/MetricSink.d.ts.map +1 -1
  118. package/dist/internal/MetricSink.js +116 -3
  119. package/dist/internal/MetricSink.js.map +1 -1
  120. package/dist/internal/Schema.js +1 -1
  121. package/dist/internal/Schema.js.map +1 -1
  122. package/dist/internal/Tidx.d.ts +1 -3
  123. package/dist/internal/Tidx.d.ts.map +1 -1
  124. package/dist/internal/Tidx.js +1 -3
  125. package/dist/internal/Tidx.js.map +1 -1
  126. package/dist/internal/Viem.d.ts.map +1 -1
  127. package/dist/internal/Viem.js +2 -1
  128. package/dist/internal/Viem.js.map +1 -1
  129. package/dist/internal/WebhookDestination.d.ts +2 -0
  130. package/dist/internal/WebhookDestination.d.ts.map +1 -1
  131. package/dist/internal/WebhookDestination.js +11 -0
  132. package/dist/internal/WebhookDestination.js.map +1 -1
  133. package/dist/internal/WebhookTransfer.d.ts +55 -0
  134. package/dist/internal/WebhookTransfer.d.ts.map +1 -0
  135. package/dist/internal/WebhookTransfer.js +296 -0
  136. package/dist/internal/WebhookTransfer.js.map +1 -0
  137. package/dist/internal/Webhooks.d.ts +37 -0
  138. package/dist/internal/Webhooks.d.ts.map +1 -1
  139. package/dist/internal/Webhooks.js +87 -13
  140. package/dist/internal/Webhooks.js.map +1 -1
  141. package/dist/internal/funding/Provider.d.ts +10 -6
  142. package/dist/internal/funding/Provider.d.ts.map +1 -1
  143. package/dist/internal/funding/Provider.js +11 -7
  144. package/dist/internal/funding/Provider.js.map +1 -1
  145. package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
  146. package/dist/internal/funding/Reconciliation.js +229 -16
  147. package/dist/internal/funding/Reconciliation.js.map +1 -1
  148. package/dist/internal/funding/providers/relay.d.ts +3 -0
  149. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  150. package/dist/internal/funding/providers/relay.js +18 -3
  151. package/dist/internal/funding/providers/relay.js.map +1 -1
  152. package/package.json +3 -3
  153. package/src/App.test.ts +32 -0
  154. package/src/App.ts +4 -1
  155. package/src/Client.test-d.ts +116 -38
  156. package/src/Webhooks.bench.ts +714 -0
  157. package/src/Webhooks.memory.bench.ts +250 -0
  158. package/src/Webhooks.test.ts +77 -6
  159. package/src/Webhooks.ts +83 -29
  160. package/src/apps/Zones.test.ts +5 -4
  161. package/src/apps/Zones.ts +9 -8
  162. package/src/apps/composition.test.ts +39 -10
  163. package/src/apps/data/FxOracle.test.ts +18 -0
  164. package/src/apps/data/FxOracle.ts +28 -5
  165. package/src/apps/data/routes/activities.test.ts +19 -0
  166. package/src/apps/data/routes/activities.ts +34 -30
  167. package/src/apps/data/routes/balances.test.ts +161 -10
  168. package/src/apps/data/routes/balances.ts +339 -147
  169. package/src/apps/data/routes/blocks.test.ts +12 -6
  170. package/src/apps/data/routes/earn.test.ts +286 -9
  171. package/src/apps/data/routes/earn.ts +841 -58
  172. package/src/apps/data/routes/exchanges.test.ts +7 -6
  173. package/src/apps/data/routes/exchanges.ts +124 -117
  174. package/src/apps/data/routes/receipts.test.ts +24 -7
  175. package/src/apps/data/routes/receipts.ts +67 -50
  176. package/src/apps/data/routes/tokens.test.ts +51 -0
  177. package/src/apps/data/routes/transactions.test.ts +42 -0
  178. package/src/apps/data/routes/transactions.ts +44 -39
  179. package/src/apps/data/routes/transfers.test.ts +402 -24
  180. package/src/apps/data/routes/transfers.ts +708 -101
  181. package/src/apps/data/routes/valuation.test.ts +26 -0
  182. package/src/apps/data/routes/valuation.ts +6 -2
  183. package/src/apps/data/routes/webhooks.test.ts +414 -130
  184. package/src/apps/data/routes/webhooks.ts +603 -419
  185. package/src/apps/data/routes/zones.ts +6 -1
  186. package/src/apps/funding/App.ts +20 -5
  187. package/src/apps/funding/routes/deposit-addresses.test.ts +43 -22
  188. package/src/apps/funding/routes/deposit-addresses.ts +34 -117
  189. package/src/apps/funding/routes/quotes.test.ts +1 -1
  190. package/src/apps/funding/routes/transfers.test.ts +6 -25
  191. package/src/apps/funding/routes/transfers.ts +5 -5
  192. package/src/apps/mpp/Mpp.test.ts +13 -1
  193. package/src/apps/mpp/Mpp.ts +8 -0
  194. package/src/apps/mpp/internal/Adapter.ts +10 -2
  195. package/src/db/Db.test.ts +40 -6
  196. package/src/db/Db.ts +42 -19
  197. package/src/db/tables/fundingDeposits.test.ts +101 -0
  198. package/src/db/tables/fundingDeposits.ts +71 -2
  199. package/src/db/tables/sponsoredTransactions.ts +9 -2
  200. package/src/db/tables/webhookSubscriptions.ts +116 -4
  201. package/src/internal/Auth.test.ts +28 -0
  202. package/src/internal/Auth.ts +15 -1
  203. package/src/internal/EarnVaults.test.ts +2 -2
  204. package/src/internal/MetricSink.test.ts +518 -0
  205. package/src/internal/MetricSink.ts +266 -9
  206. package/src/internal/Schema.ts +1 -1
  207. package/src/internal/Tidx.test.ts +4 -4
  208. package/src/internal/Tidx.ts +1 -3
  209. package/src/internal/Viem.test.ts +8 -0
  210. package/src/internal/Viem.ts +2 -1
  211. package/src/internal/WebhookDestination.test.ts +20 -0
  212. package/src/internal/WebhookDestination.ts +10 -0
  213. package/src/internal/WebhookTransfer.ts +426 -0
  214. package/src/internal/Webhooks.test.ts +137 -0
  215. package/src/internal/Webhooks.ts +131 -17
  216. package/src/internal/funding/Provider.test.ts +47 -7
  217. package/src/internal/funding/Provider.ts +16 -11
  218. package/src/internal/funding/Reconciliation.local.test.ts +3 -1
  219. package/src/internal/funding/Reconciliation.test.ts +1532 -54
  220. package/src/internal/funding/Reconciliation.ts +296 -53
  221. package/src/internal/funding/SourceObservation.test.ts +3 -2
  222. package/src/internal/funding/providers/relay.test.ts +1 -4
  223. package/src/internal/funding/providers/relay.ts +23 -3
@@ -1,6 +1,7 @@
1
1
  import type { Metrics } from '../Metrics.js'
2
2
  import type * as Webhooks from '../Webhooks.js'
3
3
  import type * as Log from './Log.js'
4
+ import * as WebhookDestination from './WebhookDestination.js'
4
5
 
5
6
  /** Webhook event type, including the synthetic `ping` carried on test envelopes. */
6
7
  export type EventType = Webhooks.Envelope['type']
@@ -8,6 +9,12 @@ export type EventType = Webhooks.Envelope['type']
8
9
  /** Where a delivery attempt originated. */
9
10
  export type Trigger = 'manual_retry' | 'ping' | 'queue'
10
11
 
12
+ /** Bounded poller stage names used by `webhook:poll`. */
13
+ export type PollStage = 'cursor_write' | 'enrich' | 'rpc_head' | 'scan' | 'subscription_read'
14
+
15
+ /** Bounded Durable Object or transport failure classes used by `webhook:poll-failure`. */
16
+ export type PollFailure = 'other' | 'overloaded' | 'remote' | 'retryable'
17
+
11
18
  /** Context for one poller tick that the {@link Webhooks.createPoller.Result} omits. */
12
19
  export type TickMeta = {
13
20
  /** Chain polled this tick; used when the tick errored before a result existed. */
@@ -20,11 +27,112 @@ export type TickMeta = {
20
27
 
21
28
  /**
22
29
  * A webhook metric event, recorded by the {@link webhooks} sink. The poller
23
- * emits `webhook:tick`/`webhook:enqueue` and the delivery path emits
24
- * `webhook:delivery`.
30
+ * emits head recovery, poll, tick, and enqueue measurements; the delivery path
31
+ * emits `webhook:delivery`.
25
32
  */
26
33
  export type WebhookEvent =
27
34
  | {
35
+ /** Tempo chain whose poll cycle completed. */
36
+ chainId: number
37
+ /** Whether the poll cycle completed without scan or dispatch failures. */
38
+ outcome: 'error' | 'ok'
39
+ /** Requested boundary movement since the preceding successful poll, in blocks. */
40
+ requestedBoundaryAdvanceBlocks?: number | undefined
41
+ /** Execution path that acquired this poll claim. */
42
+ trigger: 'alarm' | 'cron' | 'direct' | 'queue'
43
+ /** Identifies a transport-neutral poll-cycle measurement. */
44
+ type: 'webhook:poll-cycle'
45
+ }
46
+ | {
47
+ /** Tempo chain whose durable progress was read. */
48
+ chainId: number
49
+ /** Blocks between the desired and fully drained boundaries. */
50
+ lagBlocks: number
51
+ /** Identifies durable coordinator progress. */
52
+ type: 'webhook:head-progress'
53
+ }
54
+ | {
55
+ /** Tempo chain whose recovery poke was dequeued. */
56
+ chainId: number
57
+ /** When the recovery poke was dequeued, as epoch milliseconds. */
58
+ dequeuedAt: number
59
+ /** When the recovery poke was enqueued, as epoch milliseconds. */
60
+ enqueuedAt: number
61
+ /** Recovery source carried through the Queue handoff. */
62
+ trigger: 'alarm' | 'queue'
63
+ /** Identifies recovery Queue latency, not observed head volume. */
64
+ type: 'webhook:head-recovery'
65
+ }
66
+ | {
67
+ /** Tempo chain whose poll claim was acquired. */
68
+ chainId: number
69
+ /** Delay from observing the desired boundary to acquiring the claim. */
70
+ delayMs?: number | undefined
71
+ /** Execution path that acquired the claim. */
72
+ trigger: 'alarm' | 'cron' | 'direct' | 'queue'
73
+ /** Identifies a poll beginning, before scan work runs. */
74
+ type: 'webhook:poll-start'
75
+ }
76
+ | {
77
+ /** Tempo chain whose poll failed. */
78
+ chainId: number
79
+ /** Bounded class of the thrown failure. */
80
+ failure: PollFailure
81
+ /** Operation boundary that rejected the poll. */
82
+ stage: 'claim' | 'continuation_enqueue' | 'finish' | 'scan'
83
+ /** Execution path that failed before recovery. */
84
+ trigger: 'alarm' | 'cron' | 'direct' | 'queue'
85
+ /** Identifies a transport-neutral poll failure. */
86
+ type: 'webhook:poll-failure'
87
+ }
88
+ | {
89
+ chainId: number
90
+ durationMs: number
91
+ eventType?: EventType | undefined
92
+ outcome: 'error' | 'ok'
93
+ stage: PollStage
94
+ type: 'webhook:poll'
95
+ }
96
+ | {
97
+ /** Tempo chain whose eligible subscription count was read. */
98
+ chainId: number
99
+ /** Active, unexpired subscriptions selected for the poll. */
100
+ count: number
101
+ /** Identifies a pre-scan subscription eligibility measurement. */
102
+ type: 'webhook:poll-subscriptions'
103
+ }
104
+ | {
105
+ /** Chain scanned by this poll batch. */
106
+ chainId: number
107
+ /** Shared upper bound for concurrent upstream scans. */
108
+ concurrency: number
109
+ /** Scan duration, present only after completion. */
110
+ durationMs?: number | undefined
111
+ /** Subscription event type scanned by this batch. */
112
+ eventType: EventType
113
+ /** Subscriptions whose scan rejected, present only after completion. */
114
+ failedSubscriptions?: number | undefined
115
+ /** Matching rows retained by completed pages, present only after completion. */
116
+ matchedItems?: number | undefined
117
+ /** Largest block window requested by one subscription. */
118
+ maxWindowBlocks: number
119
+ /** Whether the checkpoint precedes allocation or follows scan completion. */
120
+ phase: 'complete' | 'start'
121
+ /** Outcome of the completed RPC-backed scan. */
122
+ outcome?: 'empty' | 'error' | 'missing' | 'ok' | undefined
123
+ /** JSON-RPC method used by this event type's scanner. */
124
+ rpcMethod: 'eth_getBlockByNumber' | 'eth_getLogs'
125
+ /** Subscriptions retained in this scan batch. */
126
+ subscriptionCount: number
127
+ /** Maximum subscriptions retained by this event-type batch. */
128
+ subscriptionLimit: number
129
+ /** Sum of subscription block windows in this scan batch. */
130
+ totalWindowBlocks: number
131
+ /** Identifies a memory-attribution scan boundary. */
132
+ type: 'webhook:poll-scan'
133
+ }
134
+ | {
135
+ cursorLagBlocks?: number | undefined
28
136
  meta: TickMeta
29
137
  result: Webhooks.createPoller.Result | undefined
30
138
  type: 'webhook:tick'
@@ -38,8 +146,20 @@ export type WebhookEvent =
38
146
  }
39
147
  | {
40
148
  envelope: Webhooks.Envelope
149
+ /** Cloudflare Queue delivery attempt, absent outside the Queue path. */
150
+ queueAttempt?: number | undefined
41
151
  result: Webhooks.Result
42
152
  subscription: Webhooks.Subscription
153
+ timings?: {
154
+ deliveryLogMs?: number | undefined
155
+ envelopeToQueueMs?: number | undefined
156
+ endToEndMs?: number | undefined
157
+ eventToEnvelopeMs?: number | undefined
158
+ processingMs?: number | undefined
159
+ queueWaitMs?: number | undefined
160
+ stateWriteMs?: number | undefined
161
+ subscriptionReadMs?: number | undefined
162
+ }
43
163
  trigger: Trigger
44
164
  type: 'webhook:delivery'
45
165
  }
@@ -195,7 +315,7 @@ type StatusClass = '2xx' | '4xx' | '5xx' | 'network' | 'timeout' | 'unknown' | '
195
315
  /**
196
316
  * Builds a webhook delivery-reliability sink over `backend`. Tags stay
197
317
  * low-cardinality (`chain_id`, `event_type`, `destination_type`, `status_class`,
198
- * `attempt_bucket`, `outcome`) — never subscription/org/event ids, which belong
318
+ * `attempt_bucket`, `failure`, `queue_attempt_bucket`, `outcome`, `stage`) — never subscription/org/event ids, which belong
199
319
  * in logs or analytics, not StatsD-style series.
200
320
  */
201
321
  export function webhooks(backend: Metrics): WebhookSink {
@@ -205,6 +325,101 @@ export function webhooks(backend: Metrics): WebhookSink {
205
325
  },
206
326
  record(event) {
207
327
  switch (event.type) {
328
+ case 'webhook:head-recovery': {
329
+ const tags = { chain_id: String(event.chainId), trigger: event.trigger }
330
+ backend.count('webhook_head_recovery_queue_count', 1, tags)
331
+ backend.histogram(
332
+ 'webhook_head_recovery_queue_wait_ms',
333
+ Math.max(0, event.dequeuedAt - event.enqueuedAt),
334
+ tags,
335
+ )
336
+ return
337
+ }
338
+ case 'webhook:head-progress':
339
+ backend.gauge('webhook_head_progress_lag_blocks', event.lagBlocks, {
340
+ chain_id: String(event.chainId),
341
+ })
342
+ return
343
+ case 'webhook:poll-failure':
344
+ backend.count('webhook_poll_failure_count', 1, {
345
+ chain_id: String(event.chainId),
346
+ failure: event.failure,
347
+ stage: event.stage,
348
+ trigger: event.trigger,
349
+ })
350
+ return
351
+ case 'webhook:poll': {
352
+ const tags = {
353
+ chain_id: String(event.chainId),
354
+ ...(event.eventType === undefined ? {} : { event_type: event.eventType }),
355
+ outcome: event.outcome,
356
+ stage: event.stage,
357
+ }
358
+ backend.histogram('webhook_poller_stage_duration_ms', event.durationMs, tags)
359
+ return
360
+ }
361
+ case 'webhook:poll-scan': {
362
+ const tags = {
363
+ chain_id: String(event.chainId),
364
+ event_type: event.eventType,
365
+ phase: event.phase,
366
+ }
367
+ backend.count('webhook_poller_scan_checkpoint_count', 1, tags)
368
+ backend.histogram('webhook_poller_scan_concurrency', event.concurrency, tags)
369
+ backend.histogram('webhook_poller_scan_subscription_count', event.subscriptionCount, tags)
370
+ backend.histogram('webhook_poller_scan_subscription_limit', event.subscriptionLimit, tags)
371
+ backend.histogram('webhook_poller_scan_max_window_blocks', event.maxWindowBlocks, tags)
372
+ backend.histogram(
373
+ 'webhook_poller_scan_total_window_blocks',
374
+ event.totalWindowBlocks,
375
+ tags,
376
+ )
377
+ if (event.durationMs !== undefined)
378
+ backend.histogram('webhook_poller_scan_duration_ms', event.durationMs, tags)
379
+ if (event.failedSubscriptions !== undefined)
380
+ backend.count(
381
+ 'webhook_poller_scan_failed_subscription_count',
382
+ event.failedSubscriptions,
383
+ tags,
384
+ )
385
+ if (event.matchedItems !== undefined)
386
+ backend.count('webhook_poller_scan_matched_item_count', event.matchedItems, tags)
387
+ if (event.outcome !== undefined)
388
+ backend.count('webhook_poller_rpc_scan_count', 1, {
389
+ chain_id: String(event.chainId),
390
+ event_type: event.eventType,
391
+ outcome: event.outcome,
392
+ rpc_method: event.rpcMethod,
393
+ })
394
+ return
395
+ }
396
+ case 'webhook:poll-subscriptions':
397
+ backend.gauge('webhook_poller_active_subscription_count', event.count, {
398
+ chain_id: String(event.chainId),
399
+ })
400
+ return
401
+ case 'webhook:poll-cycle': {
402
+ const tags = {
403
+ chain_id: String(event.chainId),
404
+ outcome: event.outcome,
405
+ trigger: event.trigger,
406
+ }
407
+ backend.count('webhook_poll_cycle_count', 1, tags)
408
+ if (event.requestedBoundaryAdvanceBlocks !== undefined)
409
+ backend.histogram(
410
+ 'webhook_poll_requested_boundary_advance_blocks',
411
+ event.requestedBoundaryAdvanceBlocks,
412
+ tags,
413
+ )
414
+ return
415
+ }
416
+ case 'webhook:poll-start': {
417
+ const tags = { chain_id: String(event.chainId), trigger: event.trigger }
418
+ backend.count('webhook_poll_start_count', 1, tags)
419
+ if (event.delayMs !== undefined)
420
+ backend.histogram('webhook_poll_start_delay_ms', event.delayMs, tags)
421
+ return
422
+ }
208
423
  case 'webhook:tick': {
209
424
  const { meta, result } = event
210
425
  const chain_id = String(result?.chainId ?? meta.chainId ?? 'unknown')
@@ -213,6 +428,10 @@ export function webhooks(backend: Metrics): WebhookSink {
213
428
  chain_id,
214
429
  outcome: meta.outcome,
215
430
  })
431
+ if (event.cursorLagBlocks !== undefined)
432
+ backend.gauge('webhook_subscription_cursor_lag_blocks', event.cursorLagBlocks, {
433
+ chain_id,
434
+ })
216
435
  if (!result) return
217
436
  backend.histogram('webhook_poller_subscription_count', result.subscriptions, { chain_id })
218
437
  backend.count('webhook_poller_scanned_count', result.scanned, { chain_id })
@@ -238,7 +457,14 @@ export function webhooks(backend: Metrics): WebhookSink {
238
457
  return
239
458
  }
240
459
  case 'webhook:delivery': {
241
- const { envelope, result, subscription } = event
460
+ const { envelope, result, subscription, timings } = event
461
+ const queueTags =
462
+ event.queueAttempt === undefined
463
+ ? {}
464
+ : {
465
+ queue_attempt_bucket: attemptBucket(event.queueAttempt),
466
+ queue_disposition: WebhookDestination.isRetryable(result) ? 'retried' : 'acked',
467
+ }
242
468
  const tags = {
243
469
  chain_id: String(envelope.chainId),
244
470
  destination_type: subscription.destination.type satisfies DestinationType,
@@ -249,6 +475,7 @@ export function webhooks(backend: Metrics): WebhookSink {
249
475
  ...tags,
250
476
  attempt_bucket: attemptBucket(subscription.failureCount + 1),
251
477
  outcome,
478
+ ...queueTags,
252
479
  status_class: statusClass(result),
253
480
  trigger: event.trigger,
254
481
  })
@@ -257,11 +484,41 @@ export function webhooks(backend: Metrics): WebhookSink {
257
484
  ...tags,
258
485
  outcome,
259
486
  })
260
- backend.histogram(
261
- 'webhook_delivery_queue_latency_ms',
262
- Math.max(0, Date.now() - Date.parse(envelope.createdAt)),
263
- tags,
264
- )
487
+ if (timings?.deliveryLogMs !== undefined)
488
+ backend.histogram('webhook_delivery_log_write_ms', timings.deliveryLogMs, tags)
489
+ if (timings?.envelopeToQueueMs !== undefined)
490
+ backend.histogram(
491
+ 'webhook_delivery_envelope_to_queue_ms',
492
+ timings.envelopeToQueueMs,
493
+ tags,
494
+ )
495
+ if (timings?.endToEndMs !== undefined)
496
+ backend.histogram('webhook_delivery_end_to_end_ms', timings.endToEndMs, {
497
+ ...tags,
498
+ outcome,
499
+ })
500
+ if (timings?.eventToEnvelopeMs !== undefined)
501
+ backend.histogram(
502
+ 'webhook_delivery_event_to_envelope_ms',
503
+ timings.eventToEnvelopeMs,
504
+ tags,
505
+ )
506
+ if (timings?.processingMs !== undefined)
507
+ backend.histogram('webhook_delivery_processing_ms', timings.processingMs, tags)
508
+ if (timings?.queueWaitMs !== undefined)
509
+ backend.histogram('webhook_delivery_queue_wait_ms', timings.queueWaitMs, {
510
+ ...tags,
511
+ outcome,
512
+ ...queueTags,
513
+ })
514
+ if (timings?.stateWriteMs !== undefined)
515
+ backend.histogram('webhook_delivery_state_write_ms', timings.stateWriteMs, tags)
516
+ if (timings?.subscriptionReadMs !== undefined)
517
+ backend.histogram(
518
+ 'webhook_delivery_subscription_read_ms',
519
+ timings.subscriptionReadMs,
520
+ tags,
521
+ )
265
522
  return
266
523
  }
267
524
  }
@@ -356,7 +356,7 @@ export const Denomination = z
356
356
  .check(
357
357
  z.describe(
358
358
  'Currency to denominate values in (case-insensitive, e.g. `AUD`). ' +
359
- 'Must be priced by the configured FX oracle. Defaults to `USD`.',
359
+ 'Must be priced by the configured FX oracle.',
360
360
  ),
361
361
  z.meta({ examples: ['AUD'] }),
362
362
  )
@@ -330,9 +330,9 @@ describe('resolve', () => {
330
330
  test('passes Zone metadata to the resolver', () => {
331
331
  expect(
332
332
  Tidx.resolve(({ zone }) => ({ baseUrl: zone ? 'http://tidx:8080' : undefined }), {
333
- chainId: Zones.zoneE.id,
333
+ chainId: Zones.zoneModerato.id,
334
334
  principal: null,
335
- zone: Zones.zoneE,
335
+ zone: Zones.zoneModerato,
336
336
  }),
337
337
  ).toMatchInlineSnapshot(`
338
338
  {
@@ -469,7 +469,7 @@ describe('getClient', () => {
469
469
  )
470
470
  try {
471
471
  const client = Tidx.getClient({
472
- chainId: Zones.zoneE.id,
472
+ chainId: Zones.zoneModerato.id,
473
473
  tidx: {
474
474
  baseUrl: 'https://internal.indexer',
475
475
  zoneHeaders: {
@@ -477,7 +477,7 @@ describe('getClient', () => {
477
477
  'CF-Access-Client-Secret': 'client-secret',
478
478
  },
479
479
  },
480
- zone: Zones.zoneE,
480
+ zone: Zones.zoneModerato,
481
481
  })
482
482
  await client.fetch({ query: 'select 1' })
483
483
 
@@ -92,9 +92,7 @@ export function getClient(options: getClient.Options): Client {
92
92
  * output (`baseUrl` + auth + `chainId`), not the principal, so a resolver
93
93
  * `tidx` may branch on any principal field while client cardinality stays
94
94
  * bounded by the number of distinct upstreams — callers that resolve to the same
95
- * upstream share a client. Shared by `data()` (per-request reads) and
96
- * `Webhooks.createPoller` (the scheduled poller) so the resolver logic lives in
97
- * one place.
95
+ * upstream share a client.
98
96
  */
99
97
  export function createGetClient(options: createGetClient.Options = {}): GetClient {
100
98
  const { defaultChainId = Viem.defaultChainId, tidx } = options
@@ -220,6 +220,14 @@ describe('resolveRpc', () => {
220
220
  })
221
221
 
222
222
  describe('getClient', () => {
223
+ test('disables multicall for Zones', () => {
224
+ const l1Client = Viem.getClient({ chainId: 4217 })
225
+ const zoneClient = Viem.getClient({ chainId: zone.id, zone })
226
+
227
+ expect(l1Client.batch?.multicall).toEqual({ deployless: true })
228
+ expect(zoneClient.batch?.multicall).toBe(false)
229
+ })
230
+
223
231
  test('sends inferred Bearer auth', async () => {
224
232
  const fetch = vi
225
233
  .spyOn(globalThis, 'fetch')
@@ -163,7 +163,8 @@ export function getClient(options: getClient.Options): getClient.ReturnType {
163
163
  // (e.g. `balanceOf` across many tokens) and pay one RPC round-trip.
164
164
  // Deployless mode bytecode-injects multicall3 so Tempo, which doesn't
165
165
  // configure a deployed multicall3, can still serve the aggregated call.
166
- batch: { multicall: { deployless: true } },
166
+ // Zones disable multicall entirely and issue concurrent direct `eth_call`s instead.
167
+ batch: { multicall: zone ? false : { deployless: true } },
167
168
  chain: { ...tempoMainnet, id: chainId },
168
169
  transport: http(rpcUrl, fetchOptions ? { fetchOptions } : {}),
169
170
  })
@@ -93,6 +93,26 @@ describe('from', () => {
93
93
  })
94
94
  })
95
95
 
96
+ describe('isRetryable', () => {
97
+ test.each([
98
+ ['success', { ok: true, status: 204 }, false],
99
+ ['redirect', { error: 'redirect rejected (302)', ok: false, status: 302 }, false],
100
+ ['bad request', { error: 'non-2xx response (400)', ok: false, status: 400 }, false],
101
+ ['request timeout', { error: 'non-2xx response (408)', ok: false, status: 408 }, true],
102
+ ['rate limited', { error: 'non-2xx response (429)', ok: false, status: 429 }, true],
103
+ ['server error', { error: 'non-2xx response (503)', ok: false, status: 503 }, true],
104
+ ['opaque redirect', { error: 'redirect rejected (0)', ok: false }, false],
105
+ ['timeout', { error: 'timed out after 10000ms', ok: false }, true],
106
+ ['network error', { error: 'TypeError: fetch failed', ok: false }, true],
107
+ ['unknown error', { ok: false }, true],
108
+ ] satisfies readonly [string, WebhookDestination.Result, boolean][])(
109
+ '%s',
110
+ (_label, result, retryable) => {
111
+ expect(WebhookDestination.isRetryable(result)).toBe(retryable)
112
+ },
113
+ )
114
+ })
115
+
96
116
  describe('url().deliver', () => {
97
117
  test('signs the body and POSTs a 2xx as success', async () => {
98
118
  let captured:
@@ -100,6 +100,16 @@ export type Result = {
100
100
  status?: number | undefined
101
101
  }
102
102
 
103
+ /** Returns whether another delivery attempt could plausibly succeed. */
104
+ export function isRetryable(result: Result): boolean {
105
+ if (result.ok) return false
106
+ if (result.status !== undefined)
107
+ return result.status === 408 || result.status === 429 || result.status >= 500
108
+ // Workers exposes rejected redirects as an opaque response without a status.
109
+ if (result.error && /redirect/i.test(result.error)) return false
110
+ return true
111
+ }
112
+
103
113
  /**
104
114
  * Canonical constructor: normalizes a value into a validated {@link Instance} by
105
115
  * delegating to the matching builder (`url`, `slack`, `betterStack`), each of