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
@@ -0,0 +1,326 @@
1
+ import { Hex } from 'ox'
2
+ import { encodeAbiParameters, encodeEventTopics, getAddress, parseAbiItem } from 'viem'
3
+
4
+ import * as AppWebhooks from './webhooks.js'
5
+
6
+ const sender = `0x${'aa'.repeat(20)}`
7
+ const recipient = `0x${'bb'.repeat(20)}`
8
+ const callTarget = `0x${'cc'.repeat(20)}`
9
+ const token = '0x20c0000000000000000000000000000000000001'
10
+ const transactionHash = `0x${'ef'.repeat(32)}`
11
+ const transferTopic = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
12
+ const customTopic = `0x${'12'.repeat(32)}`
13
+ // 2026-01-01T00:00:00Z in seconds.
14
+ const timestampHex = Hex.fromNumber(1_767_225_600)
15
+
16
+ const addressTopic = (address: string) => `0x${'0'.repeat(24)}${address.slice(2)}`
17
+ const oneToken = `0x${(10n ** 18n).toString(16).padStart(64, '0')}`
18
+
19
+ function transaction(overrides: Record<string, unknown> = {}): Record<string, unknown> {
20
+ return {
21
+ blockHash: `0x${'ab'.repeat(32)}`,
22
+ blockNumber: '0x7b',
23
+ calls: [{ data: '0x', input: '0x', to: callTarget, value: '0x0' }],
24
+ from: sender,
25
+ gas: '0x5208',
26
+ hash: transactionHash,
27
+ input: `0xa9059cbb${'00'.repeat(8)}`,
28
+ nonce: '0x1',
29
+ to: recipient,
30
+ transactionIndex: '0x0',
31
+ type: '0x2',
32
+ value: `0x${(10n ** 18n).toString(16)}`,
33
+ ...overrides,
34
+ }
35
+ }
36
+
37
+ function transferLog(overrides: Record<string, unknown> = {}) {
38
+ return {
39
+ address: token,
40
+ blockNumber: '0x7b',
41
+ data: oneToken,
42
+ logIndex: '0x4',
43
+ topics: [transferTopic, addressTopic(sender), addressTopic(recipient)],
44
+ transactionHash,
45
+ transactionIndex: '0x0',
46
+ ...overrides,
47
+ } as never
48
+ }
49
+
50
+ function customLog(overrides: Record<string, unknown> = {}) {
51
+ return {
52
+ address: callTarget,
53
+ blockNumber: '0x7b',
54
+ data: `0x${'0'.repeat(63)}1`,
55
+ logIndex: '0x7',
56
+ topics: [customTopic, addressTopic(sender)],
57
+ transactionHash,
58
+ transactionIndex: '0x0',
59
+ ...overrides,
60
+ } as never
61
+ }
62
+
63
+ function block(overrides: Record<string, unknown> = {}): Record<string, unknown> {
64
+ return {
65
+ gasLimit: '0x1c9c380',
66
+ gasUsed: '0x5208',
67
+ hash: `0x${'ab'.repeat(32)}`,
68
+ miner: `0x${'12'.repeat(20)}`,
69
+ number: '0x7b',
70
+ parentHash: `0x${'cd'.repeat(32)}`,
71
+ timestamp: timestampHex,
72
+ transactions: [transaction()],
73
+ ...overrides,
74
+ }
75
+ }
76
+
77
+ function subscription(eventType: AppWebhooks.EventType, filters: Record<string, unknown> = {}) {
78
+ return { eventType, filters, id: `wh_${eventType}` }
79
+ }
80
+
81
+ describe('scanBlock', () => {
82
+ test('delivers block, transaction, log, and transfer rows from one block', () => {
83
+ const result = AppWebhooks.scanBlock({
84
+ block: block(),
85
+ logs: [transferLog(), customLog()],
86
+ subscriptions: [
87
+ subscription('block:created'),
88
+ subscription('transaction:included', { from: sender }),
89
+ subscription('log:emitted', { address: callTarget, topic0: customTopic }),
90
+ subscription('token:transfer', { token }),
91
+ ],
92
+ })
93
+
94
+ expect(result.invalid).toEqual([])
95
+ expect(
96
+ result.matches.map(({ blockNumber, logIndex, subscription }) => ({
97
+ blockNumber,
98
+ logIndex,
99
+ subscription: subscription.id,
100
+ })),
101
+ ).toMatchInlineSnapshot(`
102
+ [
103
+ {
104
+ "blockNumber": 123,
105
+ "logIndex": 0,
106
+ "subscription": "wh_block:created",
107
+ },
108
+ {
109
+ "blockNumber": 123,
110
+ "logIndex": 0,
111
+ "subscription": "wh_transaction:included",
112
+ },
113
+ {
114
+ "blockNumber": 123,
115
+ "logIndex": 7,
116
+ "subscription": "wh_log:emitted",
117
+ },
118
+ {
119
+ "blockNumber": 123,
120
+ "logIndex": 4,
121
+ "subscription": "wh_token:transfer",
122
+ },
123
+ ]
124
+ `)
125
+
126
+ const [blockRow, txRow, logRow, transferRow] = result.matches.map((match) => match.data)
127
+ expect(blockRow).toMatchObject({
128
+ gasUsed: 21000,
129
+ miner: `0x${'12'.repeat(20)}`,
130
+ number: 123,
131
+ timestamp: '2026-01-01T00:00:00.000Z',
132
+ transactionCount: 1,
133
+ })
134
+ expect(txRow).toMatchObject({
135
+ hash: transactionHash,
136
+ id: transactionHash,
137
+ timestamp: '2026-01-01T00:00:00.000Z',
138
+ })
139
+ expect(logRow).toMatchObject({
140
+ address: callTarget,
141
+ logIndex: 7,
142
+ topics: [customTopic, addressTopic(sender)],
143
+ })
144
+ expect(transferRow).toMatchObject({
145
+ address: token,
146
+ amount: (10n ** 18n).toString(),
147
+ recipient,
148
+ sender,
149
+ timestamp: '2026-01-01T00:00:00.000Z',
150
+ })
151
+ })
152
+
153
+ test('evaluates every filter operator against transactions', () => {
154
+ const cases: readonly [Record<string, unknown>, boolean][] = [
155
+ [{ from: sender }, true],
156
+ [{ from: { eq: recipient } }, false],
157
+ [{ from: { not: sender } }, false],
158
+ [{ from: { not: recipient } }, true],
159
+ [{ to: { in: [callTarget, recipient] } }, true],
160
+ [{ to: { in: [callTarget] } }, true], // inner call target via includeCalls
161
+ [{ to: { in: [callTarget] }, includeCalls: false }, false],
162
+ [{ value: { gt: '0xde0b6b3a763ffff' } }, true],
163
+ [{ value: { gte: `0x${(10n ** 18n).toString(16)}` } }, true],
164
+ [{ value: { lt: '0x1' } }, false],
165
+ [{ value: { lte: `0x${(10n ** 18n).toString(16)}` } }, true],
166
+ [{ gasLimit: { gte: '0x5208' } }, true],
167
+ [{ nonce: { lte: '0x0' } }, false],
168
+ [{ input: { selector: '0xa9059cbb' } }, true],
169
+ [{ input: { startsWith: '0xa905' } }, true],
170
+ [{ hash: transactionHash }, true],
171
+ [{ calls: { to: callTarget } }, true],
172
+ [{ txType: '0x2' }, true],
173
+ [{ blockNumber: { eq: '0x7b' } }, true],
174
+ [{ timestamp: { gte: timestampHex } }, true],
175
+ ]
176
+
177
+ for (const [filters, expected] of cases) {
178
+ const result = AppWebhooks.scanBlock({
179
+ block: block(),
180
+ logs: [],
181
+ subscriptions: [subscription('transaction:included', filters)],
182
+ })
183
+ expect(result.invalid, JSON.stringify(filters)).toEqual([])
184
+ expect(result.matches.length, JSON.stringify(filters)).toBe(expected ? 1 : 0)
185
+ }
186
+ })
187
+
188
+ test('matches decoded event arguments and raw topics for logs', () => {
189
+ const signature = 'event Transfer(address indexed from, address indexed to, uint256 value)'
190
+ const scan = (filters: Record<string, unknown>) =>
191
+ AppWebhooks.scanBlock({
192
+ block: block(),
193
+ logs: [transferLog()],
194
+ subscriptions: [subscription('log:emitted', filters)],
195
+ })
196
+
197
+ const decoded = scan({ args: { from: sender }, signature })
198
+ expect(decoded.invalid).toEqual([])
199
+ expect(decoded.matches).toHaveLength(1)
200
+ expect(decoded.matches[0]?.data).toMatchObject({
201
+ // Decoding checksums addresses.
202
+ args: { from: getAddress(sender), to: getAddress(recipient), value: (10n ** 18n).toString() },
203
+ event: { signature, topic0: transferTopic },
204
+ })
205
+
206
+ expect(scan({ args: { from: recipient }, signature }).matches).toHaveLength(0)
207
+ expect(scan({ topic0: transferTopic, topic1: addressTopic(sender) }).matches).toHaveLength(1)
208
+ expect(
209
+ scan({
210
+ topic0: transferTopic,
211
+ topic1: addressTopic(sender),
212
+ topic2: { not: addressTopic(recipient) },
213
+ }).matches,
214
+ ).toHaveLength(0)
215
+ expect(scan({ address: token, topic0: transferTopic }).matches).toHaveLength(1)
216
+
217
+ // An args name absent from the ABI invalidates only that subscription.
218
+ const unknownArg = AppWebhooks.scanBlock({
219
+ block: block(),
220
+ logs: [transferLog()],
221
+ subscriptions: [
222
+ subscription('log:emitted', { args: { bogus: sender }, signature }),
223
+ subscription('token:transfer', { token }),
224
+ ],
225
+ })
226
+ expect(unknownArg.invalid).toHaveLength(1)
227
+ expect(unknownArg.matches).toHaveLength(1)
228
+
229
+ // Arg names fail closed even when the block has no decodable logs.
230
+ const unknownArgEmpty = AppWebhooks.scanBlock({
231
+ block: block(),
232
+ logs: [],
233
+ subscriptions: [subscription('log:emitted', { args: { bogus: sender }, signature })],
234
+ })
235
+ expect(unknownArgEmpty.invalid).toHaveLength(1)
236
+
237
+ const invalid = scan({ signature: 'not a signature' })
238
+ expect(invalid.matches).toHaveLength(0)
239
+ expect(invalid.invalid).toHaveLength(1)
240
+ })
241
+
242
+ test('rolls back partial matches when a matcher throws mid-block', () => {
243
+ const signature = 'event Named(string value)'
244
+ const namedTopic = encodeEventTopics({ abi: [parseAbiItem(signature)] })[0]
245
+ const namedLog = (value: string, logIndex: string) =>
246
+ ({
247
+ address: callTarget,
248
+ blockNumber: '0x7b',
249
+ data: encodeAbiParameters([{ name: 'value', type: 'string' }], [value]),
250
+ logIndex,
251
+ topics: [namedTopic],
252
+ transactionHash,
253
+ transactionIndex: '0x0',
254
+ }) as never
255
+
256
+ // The first log matches the numeric comparison; the second's nonnumeric
257
+ // string makes the matcher throw after that match was recorded.
258
+ const result = AppWebhooks.scanBlock({
259
+ block: block(),
260
+ logs: [namedLog('0x2', '0x1'), namedLog('not numeric', '0x2'), transferLog()],
261
+ subscriptions: [
262
+ subscription('log:emitted', { args: { value: { gt: '0x1' } }, signature }),
263
+ subscription('token:transfer', { token }),
264
+ ],
265
+ })
266
+ expect(result.invalid).toHaveLength(1)
267
+ expect(result.invalid[0]?.subscription.id).toBe('wh_log:emitted')
268
+ expect(result.matches.map((match) => match.subscription.id)).toEqual(['wh_token:transfer'])
269
+ })
270
+
271
+ test('matches transfers on each side and ignores non-transfer logs', () => {
272
+ const scan = (filters: Record<string, unknown>, logs = [transferLog()]) =>
273
+ AppWebhooks.scanBlock({
274
+ block: block(),
275
+ logs,
276
+ subscriptions: [subscription('token:transfer', filters)],
277
+ }).matches
278
+
279
+ expect(scan({ sender })).toHaveLength(1)
280
+ expect(scan({ recipient })).toHaveLength(1)
281
+ expect(scan({ address: sender })).toHaveLength(1)
282
+ expect(scan({ address: callTarget })).toHaveLength(0)
283
+ expect(scan({ token: '0x20c0000000000000000000000000000000000002' })).toHaveLength(0)
284
+ // Outside the TIP-20 range and malformed topics both fail decoding.
285
+ expect(scan({}, [transferLog({ address: callTarget })])).toHaveLength(0)
286
+ expect(scan({}, [transferLog({ topics: [transferTopic] })])).toHaveLength(0)
287
+ })
288
+
289
+ test('fails closed on invalid filters and unparsable blocks', () => {
290
+ const mixed = AppWebhooks.scanBlock({
291
+ block: block(),
292
+ logs: [transferLog()],
293
+ subscriptions: [
294
+ subscription('token:transfer', { token: 123 }),
295
+ subscription('token:transfer', { token }),
296
+ ],
297
+ })
298
+ expect(mixed.invalid).toHaveLength(1)
299
+ expect(mixed.invalid[0]?.subscription.filters).toEqual({ token: 123 })
300
+ expect(mixed.matches).toHaveLength(1)
301
+
302
+ const unparsable = AppWebhooks.scanBlock({
303
+ block: block({ number: undefined }),
304
+ logs: [transferLog()],
305
+ subscriptions: [subscription('block:created'), subscription('token:transfer', {})],
306
+ })
307
+ expect(unparsable.matches).toEqual([])
308
+ })
309
+
310
+ test('matches blocks on header fields and never on proposer', () => {
311
+ const scan = (filters: Record<string, unknown>) =>
312
+ AppWebhooks.scanBlock({
313
+ block: block(),
314
+ logs: [],
315
+ subscriptions: [subscription('block:created', filters)],
316
+ }).matches
317
+
318
+ expect(scan({})).toHaveLength(1)
319
+ expect(scan({ number: { eq: '0x7b' } })).toHaveLength(1)
320
+ expect(scan({ miner: `0x${'12'.repeat(20)}` })).toHaveLength(1)
321
+ expect(scan({ gasUsed: { gt: '0x5207' } })).toHaveLength(1)
322
+ expect(scan({ gasUsed: { lt: '0x5208' } })).toHaveLength(0)
323
+ expect(scan({ timestamp: { gte: timestampHex } })).toHaveLength(1)
324
+ expect(scan({ proposer: `0x${'12'.repeat(20)}` })).toHaveLength(0)
325
+ })
326
+ })
@@ -1,4 +1,4 @@
1
- import { type Db, Webhooks as Core } from 'tapimo'
1
+ import { type Db, Store, Webhooks as Core } from 'tapimo'
2
2
  import * as z from 'zod/mini'
3
3
 
4
4
  import * as Cursor from '../../../internal/Cursor.js'
@@ -1522,7 +1522,7 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1522
1522
  "logIndex": 0,
1523
1523
  "recipient": "0x9e39034aae71fb89f66061a2602eb6efec271754",
1524
1524
  "sender": "0xe7687128b0a808c2831ff94d4f7b2fb35c65af38",
1525
- "timestamp": "2026-01-14T18:38:03.000Z",
1525
+ "timestamp": "2026-01-14T18:38:03.685Z",
1526
1526
  "token": {
1527
1527
  "decimals": 6,
1528
1528
  "symbol": "QKSUSD",
@@ -1540,6 +1540,52 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1540
1540
  `)
1541
1541
  })
1542
1542
 
1543
+ test('delivers collision-shaped token metadata from a cache hit', async () => {
1544
+ const db = TestApp.database()
1545
+ const subscription = await createTransferSubscription(db)
1546
+ await Core.setCursor(db, subscription.id, windowCursor)
1547
+ const cache = Store.memory()
1548
+ const address = '0x20c00000000000000000000071de0cd31ab0d105'
1549
+ await Store.memoize(
1550
+ async () => ({
1551
+ currency: 'USD',
1552
+ decimals: 6,
1553
+ name: 'Collision token',
1554
+ symbol: '123#__bigint',
1555
+ }),
1556
+ {
1557
+ key: `webhook:token-meta:v2:42431:${address}`,
1558
+ store: cache,
1559
+ ttl: 60_000,
1560
+ },
1561
+ )
1562
+ const bodies: string[] = []
1563
+ const fetch = (async (_input, init) => {
1564
+ bodies.push(typeof init?.body === 'string' ? init.body : '')
1565
+ return new Response(null, { status: 200 })
1566
+ }) satisfies typeof globalThis.fetch
1567
+
1568
+ const result = await Webhooks.poll({
1569
+ cache,
1570
+ chainId: 42431,
1571
+ client,
1572
+ db,
1573
+ dispatch: ({ envelope, subscription }) =>
1574
+ Core.deliverAndRecord(db, subscription, envelope, { fetch }),
1575
+ eventTypes: ['token:transfer'],
1576
+ headBlockNumber: windowHead,
1577
+ })
1578
+
1579
+ const payloads = bodies.map((body) => Webhooks.schema.Envelope.parse(JSON.parse(body)))
1580
+ const delivered = payloads.find(
1581
+ (payload) => payload.type === 'token:transfer' && payload.data.address === address,
1582
+ )
1583
+ expect(result.dispatched).toBe(2)
1584
+ expect(delivered?.type === 'token:transfer' ? delivered.data.token?.symbol : undefined).toBe(
1585
+ '123#__bigint',
1586
+ )
1587
+ })
1588
+
1543
1589
  test('scans event types concurrently without increasing scan bounds', async () => {
1544
1590
  const db = TestApp.database()
1545
1591
  const transfer = await createTransferSubscription(db)
@@ -1605,7 +1651,8 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1605
1651
  chainId: 42431,
1606
1652
  client,
1607
1653
  db,
1608
- dispatchBatch: async (dispatchables) => {
1654
+ dispatchBatch: async (dispatchables, context) => {
1655
+ expect(context.db).toBe(db)
1609
1656
  batches.push([...dispatchables])
1610
1657
  return { ok: true }
1611
1658
  },
@@ -2070,7 +2117,7 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2070
2117
  "topic0": "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
2071
2118
  },
2072
2119
  "logIndex": 0,
2073
- "timestamp": "2026-01-14T18:38:03.000Z",
2120
+ "timestamp": "2026-01-14T18:38:03.685Z",
2074
2121
  "topics": [
2075
2122
  "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
2076
2123
  "0x000000000000000000000000e7687128b0a808c2831ff94d4f7b2fb35c65af38",
@@ -2113,7 +2160,7 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2113
2160
  "blockNumber": 1000002,
2114
2161
  "data": "0x0000000000000000000000000000000000000000000000000000000000002710",
2115
2162
  "logIndex": 0,
2116
- "timestamp": "2026-01-14T18:38:03.000Z",
2163
+ "timestamp": "2026-01-14T18:38:03.685Z",
2117
2164
  "topics": [
2118
2165
  "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
2119
2166
  "0x000000000000000000000000e7687128b0a808c2831ff94d4f7b2fb35c65af38",
@@ -2290,7 +2337,7 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2290
2337
  "miner": "0x0000000000000000000000000000000000000000",
2291
2338
  "number": 1000002,
2292
2339
  "parentHash": "0xa406dff9c51cc2abbef3a811f0499e99a568bf781019f129ab45dca1df0e43e5",
2293
- "timestamp": "2026-01-14T18:38:03.000Z",
2340
+ "timestamp": "2026-01-14T18:38:03.685Z",
2294
2341
  "transactionCount": 52,
2295
2342
  }
2296
2343
  `)