tapimo 0.9.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 (319) 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 +4292 -3708
  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 +21 -20
  12. package/dist/Webhooks.d.ts.map +1 -1
  13. package/dist/Webhooks.js +66 -23
  14. package/dist/Webhooks.js.map +1 -1
  15. package/dist/admin/App.d.ts +91 -84
  16. package/dist/admin/App.d.ts.map +1 -1
  17. package/dist/admin/apps/api-keys.d.ts +26 -17
  18. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  19. package/dist/admin/apps/api-keys.js +6 -0
  20. package/dist/admin/apps/api-keys.js.map +1 -1
  21. package/dist/admin/apps/organizations.d.ts +4 -2
  22. package/dist/admin/apps/organizations.d.ts.map +1 -1
  23. package/dist/admin/apps/verified-tokens.d.ts +168 -168
  24. package/dist/admin/ui.generated.d.ts +1 -1
  25. package/dist/admin/ui.generated.js +69 -69
  26. package/dist/admin/ui.generated.js.map +1 -1
  27. package/dist/analytics/Analytics.d.ts.map +1 -1
  28. package/dist/analytics/Analytics.js +26 -0
  29. package/dist/analytics/Analytics.js.map +1 -1
  30. package/dist/apps/Zones.d.ts +4280 -84
  31. package/dist/apps/Zones.d.ts.map +1 -1
  32. package/dist/apps/Zones.js +30 -10
  33. package/dist/apps/Zones.js.map +1 -1
  34. package/dist/apps/data/App.d.ts +5799 -4929
  35. package/dist/apps/data/App.d.ts.map +1 -1
  36. package/dist/apps/data/FxOracle.d.ts +4 -5
  37. package/dist/apps/data/FxOracle.d.ts.map +1 -1
  38. package/dist/apps/data/FxOracle.js +19 -4
  39. package/dist/apps/data/FxOracle.js.map +1 -1
  40. package/dist/apps/data/routes/activities.d.ts +3391 -492
  41. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  42. package/dist/apps/data/routes/activities.js +372 -29
  43. package/dist/apps/data/routes/activities.js.map +1 -1
  44. package/dist/apps/data/routes/addresses.d.ts +853 -339
  45. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  46. package/dist/apps/data/routes/balances.d.ts +10 -10
  47. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  48. package/dist/apps/data/routes/balances.js +145 -83
  49. package/dist/apps/data/routes/balances.js.map +1 -1
  50. package/dist/apps/data/routes/blocks.d.ts +123 -123
  51. package/dist/apps/data/routes/coingecko.d.ts +169 -169
  52. package/dist/apps/data/routes/earn.d.ts +455 -1
  53. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  54. package/dist/apps/data/routes/earn.js +369 -45
  55. package/dist/apps/data/routes/earn.js.map +1 -1
  56. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  57. package/dist/apps/data/routes/exchanges.js +114 -118
  58. package/dist/apps/data/routes/exchanges.js.map +1 -1
  59. package/dist/apps/data/routes/fee-amm.d.ts +24 -24
  60. package/dist/apps/data/routes/indexer.d.ts +35 -34
  61. package/dist/apps/data/routes/indexer.d.ts.map +1 -1
  62. package/dist/apps/data/routes/indexer.js +3 -2
  63. package/dist/apps/data/routes/indexer.js.map +1 -1
  64. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  65. package/dist/apps/data/routes/receipts.js +68 -47
  66. package/dist/apps/data/routes/receipts.js.map +1 -1
  67. package/dist/apps/data/routes/rpc.d.ts +31 -31
  68. package/dist/apps/data/routes/rpc.d.ts.map +1 -1
  69. package/dist/apps/data/routes/rpc.js +5 -1
  70. package/dist/apps/data/routes/rpc.js.map +1 -1
  71. package/dist/apps/data/routes/tokenlist.d.ts +12 -12
  72. package/dist/apps/data/routes/tokens.d.ts +461 -459
  73. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  74. package/dist/apps/data/routes/transactions.d.ts +313 -305
  75. package/dist/apps/data/routes/transactions.d.ts.map +1 -1
  76. package/dist/apps/data/routes/transactions.js +25 -23
  77. package/dist/apps/data/routes/transactions.js.map +1 -1
  78. package/dist/apps/data/routes/transfers.d.ts +108 -86
  79. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  80. package/dist/apps/data/routes/transfers.js +618 -105
  81. package/dist/apps/data/routes/transfers.js.map +1 -1
  82. package/dist/apps/data/routes/valuation.d.ts +21 -21
  83. package/dist/apps/data/routes/valuation.d.ts.map +1 -1
  84. package/dist/apps/data/routes/valuation.js +2 -2
  85. package/dist/apps/data/routes/valuation.js.map +1 -1
  86. package/dist/apps/data/routes/webhooks.d.ts +519 -868
  87. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  88. package/dist/apps/data/routes/webhooks.js +642 -353
  89. package/dist/apps/data/routes/webhooks.js.map +1 -1
  90. package/dist/apps/data/routes/zones.d.ts +31 -31
  91. package/dist/apps/data/routes/zones.d.ts.map +1 -1
  92. package/dist/apps/data/routes/zones.js +4 -2
  93. package/dist/apps/data/routes/zones.js.map +1 -1
  94. package/dist/apps/funding/routes/deposit-addresses.js +1 -1
  95. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
  96. package/dist/apps/funding/routes/providers.d.ts +13 -13
  97. package/dist/apps/funding/routes/quotes.d.ts +89 -89
  98. package/dist/apps/funding/routes/transfers.d.ts +36 -36
  99. package/dist/apps/funding/routes/transfers.js +1 -1
  100. package/dist/apps/funding/routes/transfers.js.map +1 -1
  101. package/dist/apps/management/App.d.ts +11 -309
  102. package/dist/apps/management/App.d.ts.map +1 -1
  103. package/dist/apps/management/routes/api-keys.d.ts +74 -74
  104. package/dist/apps/management/routes/billing.js +1 -1
  105. package/dist/apps/management/routes/billing.js.map +1 -1
  106. package/dist/apps/management/routes/faucet.d.ts +35 -35
  107. package/dist/apps/management/routes/faucet.js +2 -2
  108. package/dist/apps/management/routes/faucet.js.map +1 -1
  109. package/dist/apps/management/routes/invitations.d.ts +134 -134
  110. package/dist/apps/management/routes/invite-links.d.ts +80 -80
  111. package/dist/apps/management/routes/members.d.ts +69 -69
  112. package/dist/apps/management/routes/orgs.d.ts +98 -98
  113. package/dist/apps/management/routes/projects.d.ts +60 -60
  114. package/dist/apps/management/routes/usage.d.ts +72 -72
  115. package/dist/apps/management/routes/verified-token-requests.d.ts +83 -83
  116. package/dist/apps/management/routes/webhooks.d.ts +203 -618
  117. package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
  118. package/dist/apps/mcp.d.ts.map +1 -1
  119. package/dist/apps/mcp.js +4 -1
  120. package/dist/apps/mcp.js.map +1 -1
  121. package/dist/apps/mpp/App.d.ts +56 -44
  122. package/dist/apps/mpp/App.d.ts.map +1 -1
  123. package/dist/apps/mpp/Mpp.d.ts +3 -0
  124. package/dist/apps/mpp/Mpp.d.ts.map +1 -1
  125. package/dist/apps/mpp/Mpp.js +3 -0
  126. package/dist/apps/mpp/Mpp.js.map +1 -1
  127. package/dist/apps/mpp/internal/Adapter.d.ts.map +1 -1
  128. package/dist/apps/mpp/internal/Adapter.js +9 -2
  129. package/dist/apps/mpp/internal/Adapter.js.map +1 -1
  130. package/dist/db/Db.d.ts +4 -2
  131. package/dist/db/Db.d.ts.map +1 -1
  132. package/dist/db/Db.js +75 -15
  133. package/dist/db/Db.js.map +1 -1
  134. package/dist/db/Schema.d.ts +19 -0
  135. package/dist/db/Schema.d.ts.map +1 -1
  136. package/dist/db/Schema.js +24 -0
  137. package/dist/db/Schema.js.map +1 -1
  138. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  139. package/dist/db/tables/sponsoredTransactions.js +4 -1
  140. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  141. package/dist/db/tables/webhookQueueEvents.d.ts +122 -0
  142. package/dist/db/tables/webhookQueueEvents.d.ts.map +1 -0
  143. package/dist/db/tables/webhookQueueEvents.js +160 -0
  144. package/dist/db/tables/webhookQueueEvents.js.map +1 -0
  145. package/dist/db/tables/webhookSubscriptions.d.ts +58 -3
  146. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  147. package/dist/db/tables/webhookSubscriptions.js +86 -3
  148. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  149. package/dist/internal/Auth.d.ts +3 -0
  150. package/dist/internal/Auth.d.ts.map +1 -1
  151. package/dist/internal/Auth.js +6 -4
  152. package/dist/internal/Auth.js.map +1 -1
  153. package/dist/internal/MetricSink.d.ts +139 -3
  154. package/dist/internal/MetricSink.d.ts.map +1 -1
  155. package/dist/internal/MetricSink.js +144 -4
  156. package/dist/internal/MetricSink.js.map +1 -1
  157. package/dist/internal/RateLimit.d.ts +4 -6
  158. package/dist/internal/RateLimit.d.ts.map +1 -1
  159. package/dist/internal/RateLimit.js +15 -14
  160. package/dist/internal/RateLimit.js.map +1 -1
  161. package/dist/internal/Schema.js +1 -1
  162. package/dist/internal/Schema.js.map +1 -1
  163. package/dist/internal/Store.d.ts +2 -1
  164. package/dist/internal/Store.d.ts.map +1 -1
  165. package/dist/internal/Store.js +57 -18
  166. package/dist/internal/Store.js.map +1 -1
  167. package/dist/internal/Tidx.d.ts +1 -3
  168. package/dist/internal/Tidx.d.ts.map +1 -1
  169. package/dist/internal/Tidx.js +1 -3
  170. package/dist/internal/Tidx.js.map +1 -1
  171. package/dist/internal/Value.d.ts +2 -0
  172. package/dist/internal/Value.d.ts.map +1 -1
  173. package/dist/internal/Value.js +25 -0
  174. package/dist/internal/Value.js.map +1 -1
  175. package/dist/internal/Viem.d.ts.map +1 -1
  176. package/dist/internal/Viem.js +2 -1
  177. package/dist/internal/Viem.js.map +1 -1
  178. package/dist/internal/WebhookDestination.d.ts +2 -0
  179. package/dist/internal/WebhookDestination.d.ts.map +1 -1
  180. package/dist/internal/WebhookDestination.js +11 -0
  181. package/dist/internal/WebhookDestination.js.map +1 -1
  182. package/dist/internal/WebhookTransfer.d.ts +75 -0
  183. package/dist/internal/WebhookTransfer.d.ts.map +1 -0
  184. package/dist/internal/WebhookTransfer.js +299 -0
  185. package/dist/internal/WebhookTransfer.js.map +1 -0
  186. package/dist/internal/Webhooks.d.ts +176 -0
  187. package/dist/internal/Webhooks.d.ts.map +1 -1
  188. package/dist/internal/Webhooks.js +216 -20
  189. package/dist/internal/Webhooks.js.map +1 -1
  190. package/dist/internal/funding/Provider.d.ts +4 -0
  191. package/dist/internal/funding/Provider.d.ts.map +1 -1
  192. package/dist/internal/funding/Provider.js +11 -7
  193. package/dist/internal/funding/Provider.js.map +1 -1
  194. package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
  195. package/dist/internal/funding/Reconciliation.js +128 -0
  196. package/dist/internal/funding/Reconciliation.js.map +1 -1
  197. package/dist/internal/funding/providers/relay.d.ts +6 -4
  198. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  199. package/dist/internal/funding/providers/relay.js +71 -9
  200. package/dist/internal/funding/providers/relay.js.map +1 -1
  201. package/package.json +4 -4
  202. package/src/ApiKeys.test.ts +20 -0
  203. package/src/ApiKeys.ts +28 -1
  204. package/src/App.test.ts +10 -8
  205. package/src/App.ts +1 -0
  206. package/src/Client.test-d.ts +61 -5
  207. package/src/Webhooks.bench.ts +714 -0
  208. package/src/Webhooks.memory.bench.ts +250 -0
  209. package/src/Webhooks.test.ts +77 -6
  210. package/src/Webhooks.ts +85 -29
  211. package/src/admin/apps/api-keys.test.ts +9 -0
  212. package/src/admin/apps/api-keys.ts +15 -0
  213. package/src/admin/ui/src/lib/query-client.ts +3 -2
  214. package/src/admin/ui/src/routes/api-keys.tsx +31 -9
  215. package/src/admin/ui.generated.ts +69 -69
  216. package/src/analytics/Analytics.test.ts +8 -0
  217. package/src/analytics/Analytics.ts +26 -0
  218. package/src/apps/Zones.test.ts +23 -5
  219. package/src/apps/Zones.ts +31 -10
  220. package/src/apps/composition.test.ts +1 -0
  221. package/src/apps/data/App.test.ts +2 -2
  222. package/src/apps/data/FxOracle.test.ts +18 -0
  223. package/src/apps/data/FxOracle.ts +28 -5
  224. package/src/apps/data/routes/activities.bench.ts +41 -0
  225. package/src/apps/data/routes/activities.test.ts +122 -2
  226. package/src/apps/data/routes/activities.ts +435 -32
  227. package/src/apps/data/routes/balances.bench.ts +21 -0
  228. package/src/apps/data/routes/balances.test.ts +119 -10
  229. package/src/apps/data/routes/balances.ts +164 -97
  230. package/src/apps/data/routes/blocks.bench.ts +34 -0
  231. package/src/apps/data/routes/blocks.test.ts +12 -6
  232. package/src/apps/data/routes/coingecko.bench.ts +53 -0
  233. package/src/apps/data/routes/earn.bench.ts +71 -0
  234. package/src/apps/data/routes/earn.test.ts +197 -0
  235. package/src/apps/data/routes/earn.ts +487 -45
  236. package/src/apps/data/routes/exchanges.bench.ts +85 -0
  237. package/src/apps/data/routes/exchanges.test.ts +7 -6
  238. package/src/apps/data/routes/exchanges.ts +124 -117
  239. package/src/apps/data/routes/fee-amm.bench.ts +13 -0
  240. package/src/apps/data/routes/indexer.bench.ts +8 -0
  241. package/src/apps/data/routes/indexer.test.ts +2 -1
  242. package/src/apps/data/routes/indexer.ts +3 -2
  243. package/src/apps/data/routes/receipts.bench.ts +39 -0
  244. package/src/apps/data/routes/receipts.test.ts +24 -7
  245. package/src/apps/data/routes/receipts.ts +67 -50
  246. package/src/apps/data/routes/rpc.bench.ts +14 -0
  247. package/src/apps/data/routes/rpc.test.ts +27 -0
  248. package/src/apps/data/routes/rpc.ts +5 -1
  249. package/src/apps/data/routes/tokenlist.bench.ts +3 -0
  250. package/src/apps/data/routes/tokens.bench.ts +76 -0
  251. package/src/apps/data/routes/tokens.test.ts +51 -0
  252. package/src/apps/data/routes/transactions.bench.ts +40 -0
  253. package/src/apps/data/routes/transactions.test.ts +42 -0
  254. package/src/apps/data/routes/transactions.ts +44 -39
  255. package/src/apps/data/routes/transfers.bench.ts +28 -0
  256. package/src/apps/data/routes/transfers.test.ts +402 -24
  257. package/src/apps/data/routes/transfers.ts +708 -101
  258. package/src/apps/data/routes/valuation.bench.ts +14 -0
  259. package/src/apps/data/routes/valuation.test.ts +26 -0
  260. package/src/apps/data/routes/valuation.ts +6 -2
  261. package/src/apps/data/routes/verified-tokens.bench.ts +8 -0
  262. package/src/apps/data/routes/webhooks.bench.ts +26 -0
  263. package/src/apps/data/routes/webhooks.scan.test.ts +326 -0
  264. package/src/apps/data/routes/webhooks.test.ts +466 -135
  265. package/src/apps/data/routes/webhooks.ts +820 -429
  266. package/src/apps/data/routes/zones.bench.ts +32 -0
  267. package/src/apps/data/routes/zones.test.ts +1 -1
  268. package/src/apps/data/routes/zones.ts +7 -2
  269. package/src/apps/funding/routes/chains.test.ts +1 -1
  270. package/src/apps/funding/routes/deposit-addresses.test.ts +1 -1
  271. package/src/apps/funding/routes/deposit-addresses.ts +1 -1
  272. package/src/apps/funding/routes/providers.test.ts +1 -1
  273. package/src/apps/funding/routes/quotes.test.ts +2 -2
  274. package/src/apps/funding/routes/transfers.test.ts +3 -6
  275. package/src/apps/funding/routes/transfers.ts +1 -1
  276. package/src/apps/management/routes/billing.ts +1 -1
  277. package/src/apps/management/routes/faucet.ts +2 -2
  278. package/src/apps/management/routes/me.test.ts +31 -0
  279. package/src/apps/mcp.ts +4 -1
  280. package/src/apps/mpp/Mpp.test.ts +13 -1
  281. package/src/apps/mpp/Mpp.ts +8 -0
  282. package/src/apps/mpp/internal/Adapter.ts +10 -2
  283. package/src/db/Db.test.ts +40 -6
  284. package/src/db/Db.ts +114 -19
  285. package/src/db/Schema.ts +32 -0
  286. package/src/db/tables/sponsoredTransactions.ts +9 -2
  287. package/src/db/tables/webhookQueueEvents.ts +285 -0
  288. package/src/db/tables/webhookSubscriptions.ts +131 -4
  289. package/src/internal/Auth.test.ts +61 -50
  290. package/src/internal/Auth.ts +9 -4
  291. package/src/internal/EarnVaults.test.ts +2 -2
  292. package/src/internal/MetricSink.test.ts +575 -0
  293. package/src/internal/MetricSink.ts +325 -10
  294. package/src/internal/RateLimit.test.ts +23 -18
  295. package/src/internal/RateLimit.ts +23 -15
  296. package/src/internal/Schema.ts +1 -1
  297. package/src/internal/Store.test.ts +99 -13
  298. package/src/internal/Store.ts +52 -17
  299. package/src/internal/Tidx.test.ts +4 -4
  300. package/src/internal/Tidx.ts +1 -3
  301. package/src/internal/Value.test.ts +25 -0
  302. package/src/internal/Value.ts +29 -0
  303. package/src/internal/Viem.test.ts +8 -0
  304. package/src/internal/Viem.ts +2 -1
  305. package/src/internal/WebhookDestination.test.ts +20 -0
  306. package/src/internal/WebhookDestination.ts +10 -0
  307. package/src/internal/WebhookTransfer.test.ts +89 -0
  308. package/src/internal/WebhookTransfer.ts +430 -0
  309. package/src/internal/Webhooks.test.ts +616 -0
  310. package/src/internal/Webhooks.ts +412 -24
  311. package/src/internal/funding/Provider.test.ts +48 -8
  312. package/src/internal/funding/Provider.ts +12 -7
  313. package/src/internal/funding/Reconciliation.local.test.ts +3 -1
  314. package/src/internal/funding/Reconciliation.test.ts +1240 -0
  315. package/src/internal/funding/Reconciliation.ts +136 -0
  316. package/src/internal/funding/SourceObservation.test.ts +1 -0
  317. package/src/internal/funding/providers/index.test.ts +73 -3
  318. package/src/internal/funding/providers/relay.test.ts +2 -9
  319. package/src/internal/funding/providers/relay.ts +72 -13
@@ -0,0 +1,430 @@
1
+ import { encodeEventTopics, parseAbiItem, type RpcLog } from 'viem'
2
+ import * as z from 'zod/mini'
3
+
4
+ import * as Transfers from '../apps/data/routes/transfers.js'
5
+ import * as Schema from './Schema.js'
6
+ import * as Value from './Value.js'
7
+ import type * as Viem from './Viem.js'
8
+
9
+ /** Subscriptions matched per shared transfer scan. */
10
+ export const scanCohortSize = 32
11
+
12
+ /**
13
+ * Blocks per transfer `eth_getLogs` request. Firehose scans stop after a
14
+ * 100-row page, so small windows bound the raw RPC body retained before that cap.
15
+ */
16
+ const scanChunkBlocks = 8
17
+
18
+ const event = parseAbiItem(Transfers.signature)
19
+ const topic = encodeEventTopics({ abi: [event] })[0]
20
+
21
+ /** Parsed transfer filter predicates. */
22
+ export type Filters = z.output<typeof Transfers.schema.Predicates>
23
+
24
+ /** One decoded TIP-20 Transfer row, as the read endpoint returns it. */
25
+ export type EventData = {
26
+ address: string
27
+ amount: string
28
+ blockNumber: number
29
+ logIndex: number
30
+ recipient: string
31
+ sender: string
32
+ timestamp: string
33
+ transactionHash: string
34
+ }
35
+
36
+ type Candidate = {
37
+ index: number
38
+ key: string
39
+ state: scan.State & { window: scan.Window }
40
+ }
41
+
42
+ type Cohort = {
43
+ key: string
44
+ states: Candidate[]
45
+ window: { from: number; to: number }
46
+ }
47
+
48
+ type Accumulator = {
49
+ full: boolean
50
+ items: scan.Item[]
51
+ state: Candidate['state']
52
+ }
53
+
54
+ /** Returns a stable key for transfer filters with equivalent RPC pushdowns. */
55
+ function scanKey(filters_input: Record<string, unknown>): string {
56
+ const filters = filters_input as Filters
57
+ return JSON.stringify(
58
+ normalizeRpcValue({
59
+ args: logArgs(filters),
60
+ token: filters.token,
61
+ }),
62
+ )
63
+ }
64
+
65
+ /** Scans transfer subscriptions through shared, memory-bounded RPC cohorts. */
66
+ export async function scan(options: scan.Options): Promise<PromiseSettledResult<scan.Page>[]> {
67
+ const results: PromiseSettledResult<scan.Page>[] = []
68
+ const candidates: Candidate[] = []
69
+ for (let index = 0; index < options.states.length; index++) {
70
+ const state = options.states[index]!
71
+ if (!state.window) {
72
+ results[index] = { status: 'fulfilled', value: { items: [] } }
73
+ continue
74
+ }
75
+ candidates.push({
76
+ index,
77
+ key: scanKey(state.filters),
78
+ state: { ...state, window: state.window },
79
+ })
80
+ }
81
+
82
+ candidates.sort(
83
+ (a, b) =>
84
+ a.key.localeCompare(b.key) ||
85
+ a.state.window.from - b.state.window.from ||
86
+ a.state.window.to - b.state.window.to,
87
+ )
88
+ const cohorts: Cohort[] = []
89
+ for (const candidate of candidates) {
90
+ const previous = cohorts.at(-1)
91
+ const combinedBlocks = previous
92
+ ? Math.max(previous.window.to, candidate.state.window.to) - previous.window.from + 1
93
+ : 0
94
+ if (
95
+ !previous ||
96
+ candidate.key !== previous.key ||
97
+ candidate.state.window.from > previous.window.to ||
98
+ combinedBlocks > options.maxBlocks ||
99
+ previous.states.length >= scanCohortSize
100
+ ) {
101
+ cohorts.push({
102
+ key: candidate.key,
103
+ states: [candidate],
104
+ window: { from: candidate.state.window.from, to: candidate.state.window.to },
105
+ })
106
+ continue
107
+ }
108
+ previous.states.push(candidate)
109
+ previous.window.to = Math.max(previous.window.to, candidate.state.window.to)
110
+ }
111
+
112
+ const settled = await mapWithLimit(cohorts, options.concurrency, (cohort) =>
113
+ options.schedule(async () => ({
114
+ cohort,
115
+ pages: await scanCohort({ client: options.client, cohort }),
116
+ })),
117
+ )
118
+ for (const entry of settled) {
119
+ if (entry.status === 'rejected') continue
120
+ for (let index = 0; index < entry.value.cohort.states.length; index++)
121
+ results[entry.value.cohort.states[index]!.index] = {
122
+ status: 'fulfilled',
123
+ value: entry.value.pages[index]!,
124
+ }
125
+ }
126
+ for (let index = 0; index < settled.length; index++) {
127
+ const entry = settled[index]!
128
+ if (entry.status !== 'rejected') continue
129
+ for (const candidate of cohorts[index]!.states)
130
+ results[candidate.index] = { reason: entry.reason, status: 'rejected' }
131
+ }
132
+ return results
133
+ }
134
+
135
+ export declare namespace scan {
136
+ /** One transfer match paired with its keyset position. */
137
+ type Item = {
138
+ /** Transfer position as `[blockNumber, logIndex]`. */
139
+ cursor: readonly [number, number]
140
+ /** Decoded webhook transfer payload. */
141
+ data: unknown
142
+ }
143
+
144
+ /** One bounded page returned to the poller. */
145
+ type Page = {
146
+ /** Whether another matching page remains. */
147
+ hasMore?: boolean | undefined
148
+ /** Transfer matches in ascending chain order. */
149
+ items: readonly Item[]
150
+ /** Highest block fully scanned when no matched cursor must be retained. */
151
+ scannedThrough?: number | undefined
152
+ }
153
+
154
+ /** One subscription's inputs for a shared transfer scan. */
155
+ type State = {
156
+ /** Validated transfer subscription filters. */
157
+ filters: Record<string, unknown>
158
+ /** Maximum matches retained for this fair scan page. */
159
+ limit: number
160
+ /** Bounded chain window, or undefined when the cursor is caught up. */
161
+ window?: Window | undefined
162
+ }
163
+
164
+ /** One subscription's chain range and already-dispatched position. */
165
+ type Window = {
166
+ /** First block included in the upstream scan. */
167
+ from: number
168
+ /** Last dispatched `[blockNumber, logIndex]` position. */
169
+ skip: readonly [number, number]
170
+ /** Last block included in the upstream scan. */
171
+ to: number
172
+ }
173
+
174
+ /** Inputs for scanning transfer subscriptions. */
175
+ type Options = {
176
+ /** Maximum concurrent cohort RPC scans. */
177
+ concurrency: number
178
+ /** Viem client for raw chain RPC requests. */
179
+ client: Viem.getClient.ReturnType
180
+ /** Maximum combined block range for one cohort. */
181
+ maxBlocks: number
182
+ /** Shared scheduler that bounds upstream scans across event types. */
183
+ schedule: <value>(fn: () => Promise<value>) => Promise<value>
184
+ /** Subscription states aligned with the returned results. */
185
+ states: readonly State[]
186
+ }
187
+ }
188
+
189
+ /** Runs ordered async work with bounded concurrency. */
190
+ async function mapWithLimit<item, value>(
191
+ items: readonly item[],
192
+ limit: number,
193
+ fn: (item: item) => Promise<value>,
194
+ ): Promise<PromiseSettledResult<value>[]> {
195
+ const results: PromiseSettledResult<value>[] = []
196
+ let next = 0
197
+ await Promise.all(
198
+ Array.from({ length: Math.min(limit, items.length) }, async () => {
199
+ while (next < items.length) {
200
+ const index = next++
201
+ try {
202
+ results[index] = { status: 'fulfilled', value: await fn(items[index]!) }
203
+ } catch (reason) {
204
+ results[index] = { reason, status: 'rejected' }
205
+ }
206
+ }
207
+ }),
208
+ )
209
+ return results
210
+ }
211
+
212
+ /** Recursively canonicalizes an RPC filter value for query grouping. */
213
+ function normalizeRpcValue(value: unknown): unknown {
214
+ if (typeof value === 'bigint') return `bigint:${value}`
215
+ if (Array.isArray(value)) return value.map(normalizeRpcValue)
216
+ if (value && typeof value === 'object')
217
+ return Object.fromEntries(
218
+ Object.entries(value)
219
+ .filter(([, item]) => item !== undefined)
220
+ .sort(([a], [b]) => a.localeCompare(b))
221
+ .map(([name, item]) => [name, normalizeRpcValue(item)]),
222
+ )
223
+ return value
224
+ }
225
+
226
+ /** Returns indexed argument combinations pushed into Transfer log reads. */
227
+ function logArgs(filters: Filters) {
228
+ const args = {
229
+ ...(filters.sender ? { from: filters.sender } : {}),
230
+ ...(filters.recipient ? { to: filters.recipient } : {}),
231
+ }
232
+ return filters.address && !filters.sender && !filters.recipient
233
+ ? [{ from: filters.address }, { to: filters.address }]
234
+ : [args]
235
+ }
236
+
237
+ /** Encodes one indexed address as its left-padded event topic. */
238
+ function addressTopic(address: string): `0x${string}` {
239
+ return `0x${'0'.repeat(24)}${address.slice(2).toLowerCase()}`
240
+ }
241
+
242
+ /** Builds the exact Transfer topic filter for one indexed-argument combination. */
243
+ function topics(args: { from?: string | undefined; to?: string | undefined }) {
244
+ const values: (`0x${string}` | null)[] = [
245
+ topic,
246
+ args.from ? addressTopic(args.from) : null,
247
+ args.to ? addressTopic(args.to) : null,
248
+ ]
249
+ while (values.at(-1) === null) values.pop()
250
+ return values
251
+ }
252
+
253
+ /** Renders a block number as a JSON-RPC block tag. */
254
+ function blockTag(value: number): `0x${string}` {
255
+ return `0x${value.toString(16)}`
256
+ }
257
+
258
+ /** Issues an untyped JSON-RPC request through the Viem client. */
259
+ function rpc<result>(
260
+ client: Viem.getClient.ReturnType,
261
+ method: string,
262
+ params: readonly unknown[],
263
+ ): Promise<result> {
264
+ return (client.request as (args: { method: string; params: unknown[] }) => Promise<result>)({
265
+ method,
266
+ params: [...params],
267
+ })
268
+ }
269
+
270
+ /** Reads raw Transfer logs, splitting the either-side shortcut into two RPC filters. */
271
+ async function getLogs(
272
+ client: Viem.getClient.ReturnType,
273
+ filters: Filters,
274
+ window: { from: number; to: number },
275
+ ): Promise<RpcLog[]> {
276
+ const pages = await Promise.all(
277
+ logArgs(filters).map((args) =>
278
+ rpc<RpcLog[]>(client, 'eth_getLogs', [
279
+ {
280
+ ...(filters.token ? { address: filters.token } : {}),
281
+ fromBlock: blockTag(window.from),
282
+ topics: topics(args),
283
+ toBlock: blockTag(window.to),
284
+ },
285
+ ]),
286
+ ),
287
+ )
288
+ const logs = new Map<string, RpcLog>()
289
+ for (const page of pages)
290
+ for (const log of page)
291
+ logs.set(`${log.blockNumber}:${log.logIndex}:${log.transactionHash}`, log)
292
+ return [...logs.values()]
293
+ }
294
+
295
+ /** Sorts raw logs into ascending `(blockNumber, logIndex)` order. */
296
+ function byPosition(a: RpcLog, b: RpcLog): number {
297
+ const block = (Value.hexToNumber(a.blockNumber) ?? 0) - (Value.hexToNumber(b.blockNumber) ?? 0)
298
+ return block || (Value.hexToNumber(a.logIndex) ?? 0) - (Value.hexToNumber(b.logIndex) ?? 0)
299
+ }
300
+
301
+ /** True when an address is in Tempo's deterministic TIP-20 contract range. */
302
+ function isTip20Address(address: string): boolean {
303
+ const value = address.toLowerCase()
304
+ return value >= Transfers.tip20AddressRange[0] && value < Transfers.tip20AddressRange[1]
305
+ }
306
+
307
+ /** Decodes one strict TIP-20 Transfer wire payload. */
308
+ export function decode(log: RpcLog, timestamp: string): EventData | undefined {
309
+ if (!isTip20Address(log.address)) return undefined
310
+ if (
311
+ log.topics.length !== 3 ||
312
+ log.topics[0]?.toLowerCase() !== topic.toLowerCase() ||
313
+ !/^0x0{24}[0-9a-fA-F]{40}$/.test(log.topics[1] ?? '') ||
314
+ !/^0x0{24}[0-9a-fA-F]{40}$/.test(log.topics[2] ?? '') ||
315
+ !/^0x[0-9a-fA-F]{64}$/.test(log.data)
316
+ )
317
+ return undefined
318
+ const address = Schema.TokenAddress.safeParse(log.address)
319
+ const recipient = Schema.Address.safeParse(`0x${log.topics[2]!.slice(-40)}`)
320
+ const sender = Schema.Address.safeParse(`0x${log.topics[1]!.slice(-40)}`)
321
+ const transactionHash = Schema.Hash.safeParse(log.transactionHash)
322
+ const amount = Value.fromHex(log.data)
323
+ const blockNumber = Value.hexToNumber(log.blockNumber)
324
+ const logIndex = Value.hexToNumber(log.logIndex)
325
+ if (
326
+ !address.success ||
327
+ !recipient.success ||
328
+ !sender.success ||
329
+ !transactionHash.success ||
330
+ amount === undefined ||
331
+ blockNumber === undefined ||
332
+ logIndex === undefined
333
+ )
334
+ return undefined
335
+ return {
336
+ address: address.data,
337
+ amount,
338
+ blockNumber,
339
+ logIndex,
340
+ recipient: recipient.data,
341
+ sender: sender.data,
342
+ timestamp,
343
+ transactionHash: transactionHash.data,
344
+ }
345
+ }
346
+
347
+ /** Re-checks every transfer filter after RPC topic pushdown. */
348
+ export function matches(data: EventData, filters: Filters): boolean {
349
+ if (filters.address && data.sender !== filters.address && data.recipient !== filters.address)
350
+ return false
351
+ if (filters.recipient && data.recipient !== filters.recipient) return false
352
+ if (filters.sender && data.sender !== filters.sender) return false
353
+ if (filters.token && data.address !== filters.token) return false
354
+ return true
355
+ }
356
+
357
+ /** Adds a matched row until one subscription reaches its fair-page limit. */
358
+ function addMatch(accumulator: Accumulator, item: scan.Item): void {
359
+ if (accumulator.full) return
360
+ accumulator.items.push(item)
361
+ accumulator.full = accumulator.items.length >= accumulator.state.limit
362
+ }
363
+
364
+ /** Checks whether a canonical row remains eligible for one subscription. */
365
+ function canMatch(accumulator: Accumulator, blockNumber: number, logIndex: number): boolean {
366
+ const { skip, to } = accumulator.state.window
367
+ return (
368
+ !accumulator.full &&
369
+ blockNumber <= to &&
370
+ (blockNumber > skip[0] || (blockNumber === skip[0] && logIndex > skip[1]))
371
+ )
372
+ }
373
+
374
+ /** Converts match buffers into cursor-aware poller pages. */
375
+ function finish(accumulators: readonly Accumulator[]): scan.Page[] {
376
+ return accumulators.map((accumulator) => {
377
+ if (accumulator.full) return { hasMore: true, items: accumulator.items }
378
+ if (accumulator.items.length > 0) return { items: accumulator.items }
379
+ return { items: accumulator.items, scannedThrough: accumulator.state.window.to }
380
+ })
381
+ }
382
+
383
+ /** Scans one canonical transfer stream and fans rows into matching subscriptions. */
384
+ async function scanCohort(options: {
385
+ client: Viem.getClient.ReturnType
386
+ cohort: Cohort
387
+ }): Promise<scan.Page[]> {
388
+ const { client, cohort } = options
389
+ const accumulators = cohort.states.map<Accumulator>(({ state }) => ({
390
+ full: false,
391
+ items: [],
392
+ state,
393
+ }))
394
+ const timestamps = new Map<number, Promise<string | undefined>>()
395
+ const timestamp = (blockNumber: number) => {
396
+ const existing = timestamps.get(blockNumber)
397
+ if (existing) return existing
398
+ const pending = client
399
+ .getBlock({ blockNumber: BigInt(blockNumber), includeTransactions: false })
400
+ .then((block) => Value.blockToIso(block))
401
+ timestamps.set(blockNumber, pending)
402
+ return pending
403
+ }
404
+ const filters_source = cohort.states[0]!.state.filters as Filters
405
+ // RPC pushdowns bound selective responses; only the unfiltered firehose needs small range chunks.
406
+ const chunkBlocks = Object.values(filters_source).some(Boolean)
407
+ ? cohort.window.to - cohort.window.from + 1
408
+ : scanChunkBlocks
409
+
410
+ for (let from = cohort.window.from; from <= cohort.window.to; from += chunkBlocks) {
411
+ const to = Math.min(cohort.window.to, from + chunkBlocks - 1)
412
+ const logs = (await getLogs(client, filters_source, { from, to })).sort(byPosition)
413
+ for (const log of logs) {
414
+ const blockNumber = Value.hexToNumber(log.blockNumber)
415
+ const logIndex = Value.hexToNumber(log.logIndex)
416
+ if (blockNumber === undefined || logIndex === undefined) continue
417
+ const blockTimestamp = await timestamp(blockNumber)
418
+ if (blockTimestamp === undefined) continue
419
+ const data = decode(log, blockTimestamp)
420
+ if (!data) continue
421
+ for (const accumulator of accumulators) {
422
+ if (!canMatch(accumulator, blockNumber, logIndex)) continue
423
+ if (matches(data, accumulator.state.filters as Filters))
424
+ addMatch(accumulator, { cursor: [blockNumber, logIndex], data })
425
+ }
426
+ if (accumulators.every((accumulator) => accumulator.full)) return finish(accumulators)
427
+ }
428
+ }
429
+ return finish(accumulators)
430
+ }