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
package/src/Webhooks.ts CHANGED
@@ -1,229 +1,10 @@
1
- import * as AppWebhooks from './apps/data/routes/webhooks.js'
2
- import * as Db from './db/Db.js'
3
- import * as core_Webhooks from './internal/Webhooks.js'
4
- import * as MetricSink from './internal/MetricSink.js'
5
- import * as Store from './internal/Store.js'
6
- import type * as Metrics from './Metrics.js'
7
- import * as Viem from './internal/Viem.js'
8
-
9
- /** How often a poller reclaims expired rows, independent of tick cadence. */
10
- const pruneIntervalMs = 60_000
11
-
12
1
  // Re-export the runtime-agnostic primitives (subscriptions, signing, delivery,
13
2
  // `deliverAndRecord`, cursors, …) so consumers reach everything through one
14
- // `Webhooks` namespace. The apps-layer poller stays private behind
15
- // `createPoller` below.
3
+ // `Webhooks` namespace.
16
4
  export * from './internal/Webhooks.js'
17
5
 
18
6
  // Re-export the webhook resource's zod schemas so consumers can validate and
19
7
  // type delivery bodies: `Webhooks.schema.Envelope` (a discriminated union on
20
8
  // `type`), plus the per-event `TransferEvent` / `TransactionEvent` / `PingEvent`
21
9
  // payloads. Infer a type with `z.infer<typeof Webhooks.schema.Envelope>`.
22
- export { eventTypes, schema } from './apps/data/routes/webhooks.js'
23
-
24
- /**
25
- * Builds a webhook poller bound to a state store and chain config.
26
- *
27
- * This is the entry point a host runtime drives from chain-head notifications
28
- * or a timer. Each {@link Poller.tick} scans active subscriptions from their
29
- * cursors up to `head - confirmations` and dispatches matching events.
30
- *
31
- * Every onchain event type scans a confirmed window directly over JSON-RPC, so
32
- * the poller builds memoized viem clients via {@link Viem.createGetClient}. The
33
- * poller keeps `App.create` a pure Hono app: it constructs its own clients
34
- * instead of reaching into app internals, and owns no request state.
35
- */
36
- export function createPoller(options: createPoller.Options): createPoller.Poller {
37
- // Persistent memoize cache for token metadata, shared across ticks (the
38
- // webhook state store used to double-duty as this cache).
39
- const cache = options.cache ?? Store.memory()
40
- const getClient = Viem.createGetClient({
41
- defaultChainId: options.defaultChainId,
42
- rpc: options.rpc,
43
- })
44
- // Chain-head ticks run about once per block, so the janitor is throttled here
45
- // rather than issuing deletes per block. Reads filter expiry at query time, so
46
- // reclaiming late only costs space.
47
- let prunedAt: number | undefined
48
-
49
- const metrics = options.metrics ? MetricSink.webhooks(options.metrics) : undefined
50
-
51
- return {
52
- async tick(input = {}) {
53
- const chainId = input.chainId ?? options.defaultChainId ?? Viem.defaultChainId
54
-
55
- // Wrap the transport so each enqueue (or inline delivery) emits a timed
56
- // `webhook:enqueue` metric. The wrapper preserves the dispatch contract: it
57
- // returns the underlying result unchanged and is a no-op when no metrics
58
- // backend is configured.
59
- const inner = input.dispatch ?? options.dispatch
60
- const innerBatch =
61
- input.dispatchBatch ?? (input.dispatch === undefined ? options.dispatchBatch : undefined)
62
- const dispatch =
63
- metrics && inner
64
- ? async (dispatchable: AppWebhooks.poll.Dispatchable) => {
65
- const start = Date.now()
66
- const result = await inner(dispatchable)
67
- metrics.record({
68
- chainId: dispatchable.envelope.chainId,
69
- durationMs: Date.now() - start,
70
- eventType: dispatchable.envelope.type,
71
- ok: result.ok,
72
- type: 'webhook:enqueue',
73
- })
74
- return result
75
- }
76
- : inner
77
- const dispatchBatch = innerBatch
78
- ? async (
79
- dispatchables: readonly AppWebhooks.poll.Dispatchable[],
80
- context: AppWebhooks.poll.DispatchContext,
81
- ) => {
82
- const start = Date.now()
83
- const result = await innerBatch(dispatchables, context)
84
- const durationMs = Date.now() - start
85
- for (const dispatchable of dispatchables)
86
- metrics?.record({
87
- chainId: dispatchable.envelope.chainId,
88
- durationMs,
89
- eventType: dispatchable.envelope.type,
90
- ok: result.ok,
91
- type: 'webhook:enqueue',
92
- })
93
- return result
94
- }
95
- : undefined
96
-
97
- // Resolve per tick so Workers factories build a fresh Hyperdrive-pooled
98
- // connection each run.
99
- const db = Db.get(options.db)
100
- const now = input.now ?? options.now
101
- if (prunedAt === undefined || Date.now() - prunedAt >= pruneIntervalMs) {
102
- prunedAt = Date.now()
103
- await core_Webhooks.pruneExpired(db, { now }).catch(() => {})
104
- }
105
-
106
- const start = Date.now()
107
- let cursorLagBlocks: number | undefined
108
- try {
109
- const result = await AppWebhooks.poll({
110
- batchSize: input.batchSize ?? options.batchSize,
111
- cache,
112
- chainId,
113
- client: getClient(chainId),
114
- confirmations: input.confirmations ?? options.confirmations,
115
- db,
116
- dispatch,
117
- dispatchBatch,
118
- eventTypes: input.eventTypes ?? options.eventTypes,
119
- ...(input.headBlockNumber === undefined
120
- ? {}
121
- : { headBlockNumber: input.headBlockNumber }),
122
- maxBlocks: input.maxBlocks ?? options.maxBlocks,
123
- now,
124
- signal: input.signal,
125
- ...(metrics
126
- ? {
127
- onCursorLag(blocks: number) {
128
- cursorLagBlocks = blocks
129
- },
130
- onScanCheckpoint(checkpoint: AppWebhooks.poll.ScanCheckpoint) {
131
- metrics.record({ chainId, type: 'webhook:poll-scan', ...checkpoint })
132
- // Ship the start boundary before the RPC allocation can terminate the isolate.
133
- metrics.flush()
134
- },
135
- onStage(stage: AppWebhooks.poll.Stage) {
136
- metrics.record({ chainId, type: 'webhook:poll', ...stage })
137
- },
138
- onSubscriptions(count: number) {
139
- metrics.record({ chainId, count, type: 'webhook:poll-subscriptions' })
140
- },
141
- }
142
- : {}),
143
- })
144
- metrics?.record({
145
- cursorLagBlocks,
146
- meta: { durationMs: Date.now() - start, outcome: 'ok' },
147
- result,
148
- type: 'webhook:tick',
149
- })
150
- return result
151
- } catch (error) {
152
- metrics?.record({
153
- cursorLagBlocks,
154
- meta: { chainId, durationMs: Date.now() - start, outcome: 'error' },
155
- result: undefined,
156
- type: 'webhook:tick',
157
- })
158
- throw error
159
- } finally {
160
- metrics?.flush()
161
- }
162
- },
163
- }
164
- }
165
-
166
- export declare namespace createPoller {
167
- /** A row to dispatch, paired with its owning subscription. */
168
- type Dispatchable = AppWebhooks.poll.Dispatchable
169
-
170
- /** Delivery transport: hands a matched event to a queue or delivers inline. */
171
- type Dispatch = NonNullable<AppWebhooks.poll.Options['dispatch']>
172
-
173
- /** Batch delivery transport: confirms all rows together or leaves their cursor unchanged. */
174
- type DispatchBatch = NonNullable<AppWebhooks.poll.Options['dispatchBatch']>
175
-
176
- /** Summary counters returned by {@link Poller.tick}. */
177
- type Result = AppWebhooks.poll.Result
178
-
179
- /** Knobs shared by every {@link Poller.tick}; each is overridable per tick. */
180
- type Defaults = {
181
- /** Max matching rows scanned per subscription per tick (default 200). */
182
- batchSize?: number | undefined
183
- /** Reorg-safety window: only emit blocks this far behind head (default 1). */
184
- confirmations?: number | undefined
185
- /**
186
- * Delivery transport. Defaults to inline `deliverAndRecord` (self-host); on
187
- * Cloudflare pass a Queue producer so retry/backoff/DLQ are native.
188
- */
189
- dispatch?: Dispatch | undefined
190
- /** Batch delivery transport used when the runtime can enqueue multiple rows atomically. */
191
- dispatchBatch?: DispatchBatch | undefined
192
- /** Event types to poll (default all). */
193
- eventTypes?: AppWebhooks.poll.Options['eventTypes']
194
- /** Backfill cap: most blocks an RPC scanner inspects per tick (default 1000). */
195
- maxBlocks?: number | undefined
196
- /** Clock for envelope timestamps; injectable for tests. */
197
- now?: (() => Date) | undefined
198
- }
199
-
200
- /** Options for {@link createPoller}. */
201
- type Options = Defaults & {
202
- /** Cache for token metadata shared across ticks. */
203
- cache?: Store.Store | undefined
204
- /** Database holding subscriptions + cursors, or a factory resolved per tick (Workers/Hyperdrive). */
205
- db: Db.Source
206
- /** Tempo chain id polled when a tick omits one. */
207
- defaultChainId?: Viem.ChainId | undefined
208
- /** Metrics backend; emits `webhook:tick` per tick and `webhook:enqueue` per dispatch. */
209
- metrics?: Metrics.Metrics | undefined
210
- /** RPC options for the viem clients that scan chain events. */
211
- rpc?: Viem.getClient.Rpc | undefined
212
- }
213
-
214
- /** Per-tick overrides; falls back to the {@link Options} defaults. */
215
- type TickOptions = Defaults & {
216
- /** Chain to poll this tick (default `options.defaultChainId`). */
217
- chainId?: Viem.ChainId | undefined
218
- /** Chain head to work back from; resolved from RPC when omitted. */
219
- headBlockNumber?: bigint | number | undefined
220
- /** Stops scanning and dispatch when the caller loses its execution lease. */
221
- signal?: AbortSignal | undefined
222
- }
223
-
224
- /** A chain-head-driven webhook poller. */
225
- type Poller = {
226
- /** Runs one poll tick, resolving the RPC head when not supplied. */
227
- tick(options?: TickOptions): Promise<Result>
228
- }
229
- }
10
+ export { enrichEventData, eventTypes, scanBlock, schema } from './apps/data/routes/webhooks.js'
@@ -5,10 +5,12 @@ import * as RequestEvents from './tables/requestEvents.js'
5
5
  const context = { enabled: true, environment: 'test', service: 'api' }
6
6
 
7
7
  const entry = {
8
+ chainId: 4217,
8
9
  duration: 12.3,
9
10
  level: 'info',
10
11
  method: 'GET',
11
12
  path: '/v1/transactions',
13
+ query: 'include=receipt&limit=10',
12
14
  requestId: 'req-1',
13
15
  route: '/v1/transactions',
14
16
  status: 200,
@@ -25,6 +27,7 @@ const clickHouse = {
25
27
  /** Baseline stored row for insert/queue tests. */
26
28
  const row = {
27
29
  billing_active: null,
30
+ chain_id: 4217,
28
31
  duration_ms: 12.3,
29
32
  environment: 'test',
30
33
  error_code: null,
@@ -34,6 +37,7 @@ const row = {
34
37
  org_id: null,
35
38
  project_id: null,
36
39
  principal_type: 'unknown',
40
+ query: 'include=receipt&limit=10',
37
41
  rate_limit_scope: null,
38
42
  request_id: 'req-1',
39
43
  route: '/v1/transactions',
@@ -183,6 +187,7 @@ describe('migrations', () => {
183
187
  const sql = Analytics.migrations.map((migration) => migration.sql).join('\n')
184
188
  for (const column of Object.keys(row)) expect(sql).toContain(column)
185
189
  expect(sql).toContain('timings Map(String, Float64)')
190
+ expect(sql).toContain('query String')
186
191
  })
187
192
 
188
193
  test('the migrations declare query benchmark history', () => {
@@ -192,6 +197,22 @@ describe('migrations', () => {
192
197
  expect(sql).toContain('rpc_url String')
193
198
  expect(sql).toContain('tidx_url String')
194
199
  })
200
+
201
+ test('the migrations declare the endpoint latency view', () => {
202
+ const sql = Analytics.migrations.map((migration) => migration.sql).join('\n')
203
+ const latest = Analytics.migrations[Analytics.migrations.length - 1]!
204
+ expect(sql).toContain('CREATE OR REPLACE VIEW endpoint_latency_results')
205
+ expect(sql).toContain("'production' AS source")
206
+ expect(sql).toContain("'benchmark' AS source")
207
+ expect(sql).toContain("if(result.metric = 'benchmark', 'request', result.metric) AS metric")
208
+ expect(sql).toContain("if(status >= 200 AND status < 300, 'success', 'error') AS outcome")
209
+ expect(sql).toContain("WHERE result.metric != 'request'")
210
+ expect(sql).toContain('arrayZip(mapKeys(timings), mapValues(timings))')
211
+ expect(latest.name).toBe('0011_endpoint_latency_query')
212
+ expect(latest.sql).toContain("environment = 'production' AND service = 'cadent-api'")
213
+ expect(latest.sql).toContain("coalesce(request_id, '') AS request_id")
214
+ expect(latest.sql).toContain('query AS variant')
215
+ })
195
216
  })
196
217
 
197
218
  describe('get', () => {
@@ -213,6 +234,7 @@ describe('createQueueSink', () => {
213
234
  const row = send.mock.calls[0]![0]
214
235
  expect({
215
236
  billing_active: row.billing_active,
237
+ chain_id: row.chain_id,
216
238
  duration_ms: row.duration_ms,
217
239
  environment: row.environment,
218
240
  error_code: row.error_code,
@@ -222,6 +244,7 @@ describe('createQueueSink', () => {
222
244
  org_id: row.org_id,
223
245
  principal_type: row.principal_type,
224
246
  project_id: row.project_id,
247
+ query: row.query,
225
248
  rate_limit_scope: row.rate_limit_scope,
226
249
  request_id: row.request_id,
227
250
  route: row.route,
@@ -235,6 +258,7 @@ describe('createQueueSink', () => {
235
258
  }).toMatchInlineSnapshot(`
236
259
  {
237
260
  "billing_active": null,
261
+ "chain_id": 4217,
238
262
  "duration_ms": 12.3,
239
263
  "environment": "test",
240
264
  "error_code": null,
@@ -244,6 +268,7 @@ describe('createQueueSink', () => {
244
268
  "org_id": null,
245
269
  "principal_type": "unknown",
246
270
  "project_id": null,
271
+ "query": "include=receipt&limit=10",
247
272
  "rate_limit_scope": null,
248
273
  "request_id": "req-1",
249
274
  "route": "/v1/transactions",
@@ -270,6 +295,15 @@ describe('createQueueSink', () => {
270
295
  expect(send.mock.calls[0]![0].timings).toMatchInlineSnapshot(`{}`)
271
296
  })
272
297
 
298
+ test('stores an empty query when the request has no query string', async () => {
299
+ const send = vi.fn<(message: Analytics.Event) => Promise<void>>().mockResolvedValue(undefined)
300
+ const sink = Analytics.createQueueSink({ send })(context)
301
+
302
+ await sink({ ...entry, query: undefined })
303
+
304
+ expect(send.mock.calls[0]![0].query).toMatchInlineSnapshot(`""`)
305
+ })
306
+
273
307
  test('retries a failed send once with the same row', async () => {
274
308
  const send = vi
275
309
  .fn<(message: Analytics.Event) => Promise<void>>()
@@ -120,6 +120,7 @@ export const migrations: readonly Migration[] = [
120
120
  timestamp DateTime64(3, 'UTC'),
121
121
  service LowCardinality(String),
122
122
  environment LowCardinality(String),
123
+ chain_id Nullable(UInt32),
123
124
  method LowCardinality(String),
124
125
  route LowCardinality(String),
125
126
  status UInt16,
@@ -129,6 +130,7 @@ export const migrations: readonly Migration[] = [
129
130
  key_environment Nullable(String),
130
131
  org_id Nullable(String),
131
132
  project_id Nullable(String),
133
+ query String,
132
134
  rate_limit_scope Nullable(String),
133
135
  error_code Nullable(String),
134
136
  rpc_error_code Nullable(Int32),
@@ -203,6 +205,8 @@ export const migrations: readonly Migration[] = [
203
205
  git_ref LowCardinality(String),
204
206
  chain_id UInt32,
205
207
  chain LowCardinality(String),
208
+ method LowCardinality(String),
209
+ route LowCardinality(String),
206
210
  query String,
207
211
  metric LowCardinality(String),
208
212
  mean_ms Float64,
@@ -218,6 +222,208 @@ export const migrations: readonly Migration[] = [
218
222
  ORDER BY (chain_id, query, metric, timestamp, run_id)
219
223
  `,
220
224
  },
225
+ {
226
+ name: '0006_endpoint_latency_dimensions',
227
+ // Patches production while fresh preview tables skip the unavailable ALTER.
228
+ guard: `
229
+ SELECT toUInt8(count() = 1) FROM system.columns
230
+ WHERE database = currentDatabase()
231
+ AND table = 'request_events'
232
+ AND name = 'chain_id'
233
+ `,
234
+ sql: `
235
+ ALTER TABLE request_events
236
+ ADD COLUMN IF NOT EXISTS chain_id Nullable(UInt32) AFTER environment
237
+ `,
238
+ },
239
+ {
240
+ name: '0007_query_benchmark_dimensions',
241
+ // Patches production while fresh preview tables skip the unavailable ALTER.
242
+ guard: `
243
+ SELECT toUInt8(count() = 2) FROM system.columns
244
+ WHERE database = currentDatabase()
245
+ AND table = 'query_benchmark_results'
246
+ AND name IN ('method', 'route')
247
+ `,
248
+ sql: `
249
+ ALTER TABLE query_benchmark_results
250
+ ADD COLUMN IF NOT EXISTS method LowCardinality(String) AFTER chain,
251
+ ADD COLUMN IF NOT EXISTS route LowCardinality(String) AFTER method
252
+ `,
253
+ },
254
+ {
255
+ name: '0008_endpoint_latency_results',
256
+ sql: `
257
+ CREATE OR REPLACE VIEW endpoint_latency_results AS
258
+ SELECT
259
+ timestamp,
260
+ 'production' AS source,
261
+ chain_id,
262
+ method,
263
+ route AS endpoint,
264
+ '' AS variant,
265
+ toNullable(status) AS status,
266
+ if(status >= 200 AND status < 300, 'success', 'error') AS outcome,
267
+ measurement.1 AS metric,
268
+ measurement.2 AS mean_ms,
269
+ measurement.2 AS min_ms,
270
+ measurement.2 AS max_ms,
271
+ toUInt64(1) AS samples,
272
+ '' AS run_id,
273
+ '' AS git_sha
274
+ FROM request_events
275
+ ARRAY JOIN arrayConcat(
276
+ [('request', duration_ms)],
277
+ arrayFilter(
278
+ measurement -> measurement.1 != 'request',
279
+ arrayZip(mapKeys(timings), mapValues(timings))
280
+ )
281
+ ) AS measurement
282
+ WHERE environment = 'production' AND service = 'api'
283
+
284
+ UNION ALL
285
+
286
+ SELECT
287
+ timestamp,
288
+ 'benchmark' AS source,
289
+ toNullable(chain_id) AS chain_id,
290
+ method,
291
+ if(route = '', query, route) AS endpoint,
292
+ query AS variant,
293
+ CAST(NULL, 'Nullable(UInt16)') AS status,
294
+ 'success' AS outcome,
295
+ if(result.metric = 'benchmark', 'request', result.metric) AS metric,
296
+ mean_ms,
297
+ min_ms,
298
+ max_ms,
299
+ toUInt64(samples) AS samples,
300
+ run_id,
301
+ git_sha
302
+ FROM query_benchmark_results AS result
303
+ WHERE result.metric != 'request'
304
+ `,
305
+ },
306
+ {
307
+ name: '0009_endpoint_latency_production_service',
308
+ sql: `
309
+ CREATE OR REPLACE VIEW endpoint_latency_results AS
310
+ SELECT
311
+ timestamp,
312
+ 'production' AS source,
313
+ chain_id,
314
+ method,
315
+ route AS endpoint,
316
+ '' AS variant,
317
+ toNullable(status) AS status,
318
+ if(status >= 200 AND status < 300, 'success', 'error') AS outcome,
319
+ measurement.1 AS metric,
320
+ measurement.2 AS mean_ms,
321
+ measurement.2 AS min_ms,
322
+ measurement.2 AS max_ms,
323
+ toUInt64(1) AS samples,
324
+ '' AS run_id,
325
+ '' AS git_sha
326
+ FROM request_events
327
+ ARRAY JOIN arrayConcat(
328
+ [('request', duration_ms)],
329
+ arrayFilter(
330
+ measurement -> measurement.1 != 'request',
331
+ arrayZip(mapKeys(timings), mapValues(timings))
332
+ )
333
+ ) AS measurement
334
+ WHERE environment = 'production' AND service = 'cadent-api'
335
+
336
+ UNION ALL
337
+
338
+ SELECT
339
+ timestamp,
340
+ 'benchmark' AS source,
341
+ toNullable(chain_id) AS chain_id,
342
+ method,
343
+ if(route = '', query, route) AS endpoint,
344
+ query AS variant,
345
+ CAST(NULL, 'Nullable(UInt16)') AS status,
346
+ 'success' AS outcome,
347
+ if(result.metric = 'benchmark', 'request', result.metric) AS metric,
348
+ mean_ms,
349
+ min_ms,
350
+ max_ms,
351
+ toUInt64(samples) AS samples,
352
+ run_id,
353
+ git_sha
354
+ FROM query_benchmark_results AS result
355
+ WHERE result.metric != 'request'
356
+ `,
357
+ },
358
+ {
359
+ name: '0010_request_events_query',
360
+ // Patches tables created before redacted request queries entered the baseline.
361
+ // Fresh tables skip this ALTER, allowing scoped preview users to migrate.
362
+ guard: `
363
+ SELECT toUInt8(count() = 1) FROM system.columns
364
+ WHERE database = currentDatabase()
365
+ AND table = 'request_events'
366
+ AND name = 'query'
367
+ `,
368
+ sql: `
369
+ ALTER TABLE request_events
370
+ ADD COLUMN IF NOT EXISTS query String AFTER project_id
371
+ `,
372
+ },
373
+ {
374
+ name: '0011_endpoint_latency_query',
375
+ sql: `
376
+ CREATE OR REPLACE VIEW endpoint_latency_results AS
377
+ SELECT
378
+ timestamp,
379
+ 'production' AS source,
380
+ chain_id,
381
+ method,
382
+ route AS endpoint,
383
+ query AS variant,
384
+ toNullable(status) AS status,
385
+ if(status >= 200 AND status < 300, 'success', 'error') AS outcome,
386
+ measurement.1 AS metric,
387
+ measurement.2 AS mean_ms,
388
+ measurement.2 AS min_ms,
389
+ measurement.2 AS max_ms,
390
+ toUInt64(1) AS samples,
391
+ coalesce(request_id, '') AS request_id,
392
+ '' AS run_id,
393
+ '' AS git_sha
394
+ FROM request_events
395
+ ARRAY JOIN arrayConcat(
396
+ [('request', duration_ms)],
397
+ arrayFilter(
398
+ measurement -> measurement.1 != 'request',
399
+ arrayZip(mapKeys(timings), mapValues(timings))
400
+ )
401
+ ) AS measurement
402
+ WHERE environment = 'production' AND service = 'cadent-api'
403
+
404
+ UNION ALL
405
+
406
+ SELECT
407
+ timestamp,
408
+ 'benchmark' AS source,
409
+ toNullable(chain_id) AS chain_id,
410
+ method,
411
+ if(route = '', query, route) AS endpoint,
412
+ query AS variant,
413
+ CAST(NULL, 'Nullable(UInt16)') AS status,
414
+ 'success' AS outcome,
415
+ if(result.metric = 'benchmark', 'request', result.metric) AS metric,
416
+ mean_ms,
417
+ min_ms,
418
+ max_ms,
419
+ toUInt64(samples) AS samples,
420
+ '' AS request_id,
421
+ run_id,
422
+ git_sha
423
+ FROM query_benchmark_results AS result
424
+ WHERE result.metric != 'request'
425
+ `,
426
+ },
221
427
  ]
222
428
 
223
429
  /**
@@ -10,6 +10,8 @@ export type Table = {
10
10
  * billing gate for sandbox: only `1` rows bill.
11
11
  */
12
12
  billing_active: number | null
13
+ /** Tempo chain selected for the request. */
14
+ chain_id: number | null
13
15
  /** Total request duration in milliseconds. */
14
16
  duration_ms: number
15
17
  /** Deployment environment that served the request. */
@@ -28,6 +30,8 @@ export type Table = {
28
30
  project_id: string | null
29
31
  /** Caller principal kind. */
30
32
  principal_type: 'api_key' | 'public' | 'session' | 'super_admin' | 'unknown'
33
+ /** Redacted request query string, without a leading `?`. */
34
+ query: string
31
35
  /** Rate-limit bucket scope, when a rate limit was applied. */
32
36
  rate_limit_scope: string | null
33
37
  /** Numeric JSON-RPC error code when every failed response shares one code. */
@@ -557,6 +561,7 @@ function event(entry: Log.Entry, context: Metrics.cloudflare.Context): Table {
557
561
  const principal = entry.principal
558
562
  return {
559
563
  billing_active: principal?.type === 'api_key' ? Number(principal.billingActive === true) : null,
564
+ chain_id: entry.chainId ?? null,
560
565
  duration_ms: entry.duration,
561
566
  environment: context.environment ?? 'unknown',
562
567
  error_code: entry.errorCode ?? null,
@@ -566,6 +571,7 @@ function event(entry: Log.Entry, context: Metrics.cloudflare.Context): Table {
566
571
  org_id: principal?.type === 'api_key' ? (principal.orgId ?? null) : null,
567
572
  project_id: principal?.type === 'api_key' ? (principal.projectId ?? null) : null,
568
573
  principal_type: principal?.type ?? 'unknown',
574
+ query: entry.query ?? '',
569
575
  rate_limit_scope: entry.rateLimit?.scope ?? null,
570
576
  request_id: entry.requestId ?? null,
571
577
  route: entry.route,
@@ -326,6 +326,7 @@ describe('funding()', () => {
326
326
  ),
327
327
  ),
328
328
  ).toEqual([
329
+ 'listFundingDepositAddresses',
329
330
  'createFundingDepositAddress',
330
331
  'reconcileFundingDepositAddress',
331
332
  'getFundingDepositAddress',
@@ -224,7 +224,13 @@ export function data(options: data.Options = {}) {
224
224
  .route('/', transactionActivities({ fx: options.fx }))
225
225
  .route('/', transfers({ fx: options.fx }))
226
226
  .route('/', verifiedTokens())
227
- .route('/', webhooks({ enabled: options.webhook !== false }))
227
+ .route(
228
+ '/',
229
+ webhooks({
230
+ applicationEventTypes: webhook?.applicationEventTypes ?? [],
231
+ enabled: options.webhook !== false,
232
+ }),
233
+ )
228
234
  .route('/', zones())
229
235
  .route('/', rpc())
230
236
  .route('/', coingecko())
@@ -237,6 +243,8 @@ export function data(options: data.Options = {}) {
237
243
  indexerHandlers: indexerRouteHandlers,
238
244
  rpcOnlyHandlers: rpcOnlyRouteHandlers,
239
245
  })
246
+ if (selection.matched && !selection.conflict && !selection.invalid)
247
+ c.set('chainId', selection.chainId)
240
248
  return (
241
249
  selection.matched &&
242
250
  !selection.conflict &&
@@ -443,7 +451,7 @@ function webhooksBlock() {
443
451
  'tempo-event-id',
444
452
  'A unique, stable id for this event (`evt_…`). The same event may be delivered more than once, so use this id to skip duplicates.',
445
453
  ),
446
- header('tempo-event-type', 'What happened: `token:transfer`, `transaction:included`, `log:emitted`, `block:created`, or `ping`.'), // prettier-ignore
454
+ header('tempo-event-type', 'What happened: `token:transfer`, `transaction:included`, `log:emitted`, `block:created`, `funding:deposit.updated`, or `ping`.'), // prettier-ignore
447
455
  ],
448
456
  requestBody: {
449
457
  content: { 'application/json': { schema: envelope } },