tapimo 0.5.3 → 0.7.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 (376) hide show
  1. package/dist/App.d.ts +15830 -7220
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +9 -1
  4. package/dist/App.js.map +1 -1
  5. package/dist/Scope.d.ts +4 -0
  6. package/dist/Scope.d.ts.map +1 -1
  7. package/dist/Scope.js +5 -0
  8. package/dist/Scope.js.map +1 -1
  9. package/dist/admin/App.d.ts +1011 -412
  10. package/dist/admin/App.d.ts.map +1 -1
  11. package/dist/admin/App.js +4 -0
  12. package/dist/admin/App.js.map +1 -1
  13. package/dist/admin/apps/api-keys.d.ts +2 -1
  14. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  15. package/dist/admin/apps/early-access.d.ts +148 -0
  16. package/dist/admin/apps/early-access.d.ts.map +1 -0
  17. package/dist/admin/apps/early-access.js +133 -0
  18. package/dist/admin/apps/early-access.js.map +1 -0
  19. package/dist/admin/apps/organizations.d.ts +124 -1
  20. package/dist/admin/apps/organizations.d.ts.map +1 -1
  21. package/dist/admin/apps/organizations.js +97 -6
  22. package/dist/admin/apps/organizations.js.map +1 -1
  23. package/dist/admin/apps/verified-tokens.d.ts +526 -1
  24. package/dist/admin/apps/verified-tokens.d.ts.map +1 -1
  25. package/dist/admin/apps/verified-tokens.js +214 -1
  26. package/dist/admin/apps/verified-tokens.js.map +1 -1
  27. package/dist/admin/ui.generated.d.ts +1 -1
  28. package/dist/admin/ui.generated.d.ts.map +1 -1
  29. package/dist/admin/ui.generated.js +95 -75
  30. package/dist/admin/ui.generated.js.map +1 -1
  31. package/dist/analytics/Analytics.d.ts.map +1 -1
  32. package/dist/analytics/Analytics.js +20 -0
  33. package/dist/analytics/Analytics.js.map +1 -1
  34. package/dist/analytics/tables/requestEvents.d.ts +18 -0
  35. package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
  36. package/dist/analytics/tables/requestEvents.js +36 -7
  37. package/dist/analytics/tables/requestEvents.js.map +1 -1
  38. package/dist/apps/data/App.d.ts +6260 -4924
  39. package/dist/apps/data/App.d.ts.map +1 -1
  40. package/dist/apps/data/App.js +11 -6
  41. package/dist/apps/data/App.js.map +1 -1
  42. package/dist/apps/data/FundingQuoteProviders.d.ts +34 -1
  43. package/dist/apps/data/FundingQuoteProviders.d.ts.map +1 -1
  44. package/dist/apps/data/FundingQuoteProviders.js +194 -16
  45. package/dist/apps/data/FundingQuoteProviders.js.map +1 -1
  46. package/dist/apps/data/FxOracle.d.ts +71 -0
  47. package/dist/apps/data/FxOracle.d.ts.map +1 -0
  48. package/dist/apps/data/FxOracle.js +97 -0
  49. package/dist/apps/data/FxOracle.js.map +1 -0
  50. package/dist/apps/data/index.d.ts +1 -0
  51. package/dist/apps/data/index.d.ts.map +1 -1
  52. package/dist/apps/data/index.js +1 -0
  53. package/dist/apps/data/index.js.map +1 -1
  54. package/dist/apps/data/routes/activities.d.ts +3599 -1382
  55. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  56. package/dist/apps/data/routes/activities.js +250 -88
  57. package/dist/apps/data/routes/activities.js.map +1 -1
  58. package/dist/apps/data/routes/addresses.d.ts +872 -251
  59. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  60. package/dist/apps/data/routes/addresses.js +11 -9
  61. package/dist/apps/data/routes/addresses.js.map +1 -1
  62. package/dist/apps/data/routes/balances.d.ts +77 -26
  63. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  64. package/dist/apps/data/routes/balances.js +88 -45
  65. package/dist/apps/data/routes/balances.js.map +1 -1
  66. package/dist/apps/data/routes/blocks.d.ts +39 -39
  67. package/dist/apps/data/routes/coingecko.d.ts +54 -54
  68. package/dist/apps/data/routes/exchanges.d.ts +856 -105
  69. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  70. package/dist/apps/data/routes/exchanges.js +518 -76
  71. package/dist/apps/data/routes/exchanges.js.map +1 -1
  72. package/dist/apps/data/routes/fee-amm.d.ts +81 -67
  73. package/dist/apps/data/routes/fee-amm.d.ts.map +1 -1
  74. package/dist/apps/data/routes/fee-amm.js +76 -91
  75. package/dist/apps/data/routes/fee-amm.js.map +1 -1
  76. package/dist/apps/data/routes/funding.d.ts +80 -36
  77. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  78. package/dist/apps/data/routes/funding.js +65 -31
  79. package/dist/apps/data/routes/funding.js.map +1 -1
  80. package/dist/apps/data/routes/receipts.d.ts +392 -315
  81. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  82. package/dist/apps/data/routes/receipts.js +152 -77
  83. package/dist/apps/data/routes/receipts.js.map +1 -1
  84. package/dist/apps/data/routes/tokens.d.ts +167 -174
  85. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  86. package/dist/apps/data/routes/tokens.js +15 -13
  87. package/dist/apps/data/routes/tokens.js.map +1 -1
  88. package/dist/apps/data/routes/transactions.d.ts +277 -536
  89. package/dist/apps/data/routes/transactions.d.ts.map +1 -1
  90. package/dist/apps/data/routes/transactions.js +96 -77
  91. package/dist/apps/data/routes/transactions.js.map +1 -1
  92. package/dist/apps/data/routes/transfers.d.ts +157 -81
  93. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  94. package/dist/apps/data/routes/transfers.js +272 -130
  95. package/dist/apps/data/routes/transfers.js.map +1 -1
  96. package/dist/apps/data/routes/valuation.d.ts +307 -0
  97. package/dist/apps/data/routes/valuation.d.ts.map +1 -0
  98. package/dist/apps/data/routes/valuation.js +292 -0
  99. package/dist/apps/data/routes/valuation.js.map +1 -0
  100. package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
  101. package/dist/apps/data/routes/verified-tokens.d.ts.map +1 -1
  102. package/dist/apps/data/routes/verified-tokens.js +7 -5
  103. package/dist/apps/data/routes/verified-tokens.js.map +1 -1
  104. package/dist/apps/data/routes/webhooks.d.ts +103 -123
  105. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  106. package/dist/apps/data/routes/webhooks.js +124 -25
  107. package/dist/apps/data/routes/webhooks.js.map +1 -1
  108. package/dist/apps/index.d.ts +2 -1
  109. package/dist/apps/index.d.ts.map +1 -1
  110. package/dist/apps/index.js +2 -1
  111. package/dist/apps/index.js.map +1 -1
  112. package/dist/apps/management/App.d.ts +8554 -1692
  113. package/dist/apps/management/App.d.ts.map +1 -1
  114. package/dist/apps/management/App.js +12 -1
  115. package/dist/apps/management/App.js.map +1 -1
  116. package/dist/apps/management/routes/api-keys.d.ts +7 -0
  117. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  118. package/dist/apps/management/routes/api-keys.js +17 -3
  119. package/dist/apps/management/routes/api-keys.js.map +1 -1
  120. package/dist/apps/management/routes/billing.d.ts +157 -162
  121. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  122. package/dist/apps/management/routes/billing.js +31 -23
  123. package/dist/apps/management/routes/billing.js.map +1 -1
  124. package/dist/apps/management/routes/faucet.d.ts +232 -0
  125. package/dist/apps/management/routes/faucet.d.ts.map +1 -0
  126. package/dist/apps/management/routes/faucet.js +154 -0
  127. package/dist/apps/management/routes/faucet.js.map +1 -0
  128. package/dist/apps/management/routes/invitations.d.ts +60 -60
  129. package/dist/apps/management/routes/invite-links.d.ts +49 -20
  130. package/dist/apps/management/routes/invite-links.d.ts.map +1 -1
  131. package/dist/apps/management/routes/invite-links.js +26 -1
  132. package/dist/apps/management/routes/invite-links.js.map +1 -1
  133. package/dist/apps/management/routes/me.d.ts +6 -6
  134. package/dist/apps/management/routes/members.d.ts +6 -6
  135. package/dist/apps/management/routes/orgs.d.ts +42 -42
  136. package/dist/apps/management/routes/orgs.d.ts.map +1 -1
  137. package/dist/apps/management/routes/orgs.js +5 -0
  138. package/dist/apps/management/routes/orgs.js.map +1 -1
  139. package/dist/apps/management/routes/projects.d.ts +44 -44
  140. package/dist/apps/management/routes/usage.d.ts +3 -0
  141. package/dist/apps/management/routes/usage.d.ts.map +1 -1
  142. package/dist/apps/management/routes/usage.js +4 -0
  143. package/dist/apps/management/routes/usage.js.map +1 -1
  144. package/dist/apps/management/routes/verified-token-requests.d.ts +697 -0
  145. package/dist/apps/management/routes/verified-token-requests.d.ts.map +1 -0
  146. package/dist/apps/management/routes/verified-token-requests.js +271 -0
  147. package/dist/apps/management/routes/verified-token-requests.js.map +1 -0
  148. package/dist/apps/management/routes/webhooks.d.ts +6977 -0
  149. package/dist/apps/management/routes/webhooks.d.ts.map +1 -0
  150. package/dist/apps/management/routes/webhooks.js +573 -0
  151. package/dist/apps/management/routes/webhooks.js.map +1 -0
  152. package/dist/apps/mpp/App.d.ts +363 -0
  153. package/dist/apps/mpp/App.d.ts.map +1 -0
  154. package/dist/apps/mpp/App.js +72 -0
  155. package/dist/apps/mpp/App.js.map +1 -0
  156. package/dist/apps/mpp/Mpp.d.ts +178 -0
  157. package/dist/apps/mpp/Mpp.d.ts.map +1 -0
  158. package/dist/apps/mpp/Mpp.js +183 -0
  159. package/dist/apps/mpp/Mpp.js.map +1 -0
  160. package/dist/apps/mpp/index.d.ts +3 -0
  161. package/dist/apps/mpp/index.d.ts.map +1 -0
  162. package/dist/apps/mpp/index.js +3 -0
  163. package/dist/apps/mpp/index.js.map +1 -0
  164. package/dist/apps/relay/App.d.ts.map +1 -1
  165. package/dist/apps/relay/App.js +56 -9
  166. package/dist/apps/relay/App.js.map +1 -1
  167. package/dist/db/Db.d.ts +6 -0
  168. package/dist/db/Db.d.ts.map +1 -1
  169. package/dist/db/Db.js +110 -0
  170. package/dist/db/Db.js.map +1 -1
  171. package/dist/db/Schema.d.ts +53 -0
  172. package/dist/db/Schema.d.ts.map +1 -1
  173. package/dist/db/Schema.js +56 -0
  174. package/dist/db/Schema.js.map +1 -1
  175. package/dist/db/tables/earlyAccess.d.ts +50 -0
  176. package/dist/db/tables/earlyAccess.d.ts.map +1 -0
  177. package/dist/db/tables/earlyAccess.js +63 -0
  178. package/dist/db/tables/earlyAccess.js.map +1 -0
  179. package/dist/db/tables/enabledBillingSources.d.ts +73 -0
  180. package/dist/db/tables/enabledBillingSources.d.ts.map +1 -0
  181. package/dist/db/tables/enabledBillingSources.js +69 -0
  182. package/dist/db/tables/enabledBillingSources.js.map +1 -0
  183. package/dist/db/tables/invitations.d.ts.map +1 -1
  184. package/dist/db/tables/invitations.js +3 -0
  185. package/dist/db/tables/invitations.js.map +1 -1
  186. package/dist/db/tables/inviteLinks.d.ts +10 -2
  187. package/dist/db/tables/inviteLinks.d.ts.map +1 -1
  188. package/dist/db/tables/inviteLinks.js +10 -1
  189. package/dist/db/tables/inviteLinks.js.map +1 -1
  190. package/dist/db/tables/organizations.d.ts +5 -2
  191. package/dist/db/tables/organizations.d.ts.map +1 -1
  192. package/dist/db/tables/organizations.js +13 -2
  193. package/dist/db/tables/organizations.js.map +1 -1
  194. package/dist/db/tables/sponsoredTransactions.d.ts +2 -0
  195. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  196. package/dist/db/tables/sponsoredTransactions.js +2 -0
  197. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  198. package/dist/db/tables/verifiedTokenRequests.d.ts +94 -0
  199. package/dist/db/tables/verifiedTokenRequests.d.ts.map +1 -0
  200. package/dist/db/tables/verifiedTokenRequests.js +120 -0
  201. package/dist/db/tables/verifiedTokenRequests.js.map +1 -0
  202. package/dist/db/tables/webhookSubscriptions.d.ts +14 -0
  203. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  204. package/dist/db/tables/webhookSubscriptions.js +16 -0
  205. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  206. package/dist/handlers/internal/utils.d.ts +1 -16
  207. package/dist/handlers/internal/utils.d.ts.map +1 -1
  208. package/dist/handlers/internal/utils.js +17 -6
  209. package/dist/handlers/internal/utils.js.map +1 -1
  210. package/dist/handlers/relay.d.ts.map +1 -1
  211. package/dist/handlers/relay.js +37 -5
  212. package/dist/handlers/relay.js.map +1 -1
  213. package/dist/internal/Auth.d.ts +11 -2
  214. package/dist/internal/Auth.d.ts.map +1 -1
  215. package/dist/internal/Auth.js +14 -47
  216. package/dist/internal/Auth.js.map +1 -1
  217. package/dist/internal/FundingQuotes.d.ts +147 -47
  218. package/dist/internal/FundingQuotes.d.ts.map +1 -1
  219. package/dist/internal/FundingQuotes.js +119 -33
  220. package/dist/internal/FundingQuotes.js.map +1 -1
  221. package/dist/internal/Log.d.ts +37 -5
  222. package/dist/internal/Log.d.ts.map +1 -1
  223. package/dist/internal/Log.js +52 -3
  224. package/dist/internal/Log.js.map +1 -1
  225. package/dist/internal/MetricSink.d.ts.map +1 -1
  226. package/dist/internal/MetricSink.js +24 -0
  227. package/dist/internal/MetricSink.js.map +1 -1
  228. package/dist/internal/OpenApi.d.ts +2 -2
  229. package/dist/internal/OpenApi.d.ts.map +1 -1
  230. package/dist/internal/OpenApi.js +3 -0
  231. package/dist/internal/OpenApi.js.map +1 -1
  232. package/dist/internal/Response.d.ts +2 -2
  233. package/dist/internal/Response.d.ts.map +1 -1
  234. package/dist/internal/Response.js +12 -4
  235. package/dist/internal/Response.js.map +1 -1
  236. package/dist/internal/Schema.d.ts +14 -5
  237. package/dist/internal/Schema.d.ts.map +1 -1
  238. package/dist/internal/Schema.js +23 -5
  239. package/dist/internal/Schema.js.map +1 -1
  240. package/dist/internal/Value.d.ts +18 -0
  241. package/dist/internal/Value.d.ts.map +1 -1
  242. package/dist/internal/Value.js +11 -1
  243. package/dist/internal/Value.js.map +1 -1
  244. package/dist/internal/VerifiedTokens.d.ts.map +1 -1
  245. package/dist/internal/VerifiedTokens.js +10 -1
  246. package/dist/internal/VerifiedTokens.js.map +1 -1
  247. package/dist/internal/Webhooks.d.ts +24 -3
  248. package/dist/internal/Webhooks.d.ts.map +1 -1
  249. package/dist/internal/Webhooks.js +34 -8
  250. package/dist/internal/Webhooks.js.map +1 -1
  251. package/package.json +7 -2
  252. package/src/App.test.ts +9 -4
  253. package/src/App.ts +23 -3
  254. package/src/Client.test-d.ts +733 -20
  255. package/src/Scope.ts +5 -0
  256. package/src/admin/App.test.ts +4 -0
  257. package/src/admin/App.ts +4 -0
  258. package/src/admin/apps/early-access.test.ts +125 -0
  259. package/src/admin/apps/early-access.ts +176 -0
  260. package/src/admin/apps/organizations.test.ts +86 -2
  261. package/src/admin/apps/organizations.ts +155 -31
  262. package/src/admin/apps/scopes.test.ts +5 -0
  263. package/src/admin/apps/verified-tokens.test.ts +201 -0
  264. package/src/admin/apps/verified-tokens.ts +532 -286
  265. package/src/admin/ui/plugin.ts +2 -1
  266. package/src/admin/ui/src/lib/early-access.ts +63 -0
  267. package/src/admin/ui/src/lib/organizations.test.ts +81 -1
  268. package/src/admin/ui/src/lib/organizations.ts +92 -0
  269. package/src/admin/ui/src/lib/verified-tokens.ts +79 -0
  270. package/src/admin/ui/src/routeTree.gen.ts +33 -2
  271. package/src/admin/ui/src/routes/__root.tsx +2 -1
  272. package/src/admin/ui/src/routes/api-keys.tsx +1 -1
  273. package/src/admin/ui/src/routes/early-access.tsx +162 -0
  274. package/src/admin/ui/src/routes/organizations_.$orgId.tsx +103 -0
  275. package/src/admin/ui/src/routes/verified-tokens.tsx +230 -19
  276. package/src/admin/ui.generated.ts +95 -94
  277. package/src/analytics/Analytics.test.ts +83 -0
  278. package/src/analytics/Analytics.ts +20 -0
  279. package/src/analytics/tables/requestEvents.ts +60 -7
  280. package/src/apps/composition.test.ts +11 -2
  281. package/src/apps/data/App.test.ts +1 -1
  282. package/src/apps/data/App.ts +22 -14
  283. package/src/apps/data/FundingQuoteProviders.test.ts +297 -4
  284. package/src/apps/data/FundingQuoteProviders.ts +255 -29
  285. package/src/apps/data/FxOracle.test.ts +118 -0
  286. package/src/apps/data/FxOracle.ts +153 -0
  287. package/src/apps/data/index.ts +1 -0
  288. package/src/apps/data/routes/activities.test.ts +177 -3
  289. package/src/apps/data/routes/activities.ts +441 -132
  290. package/src/apps/data/routes/addresses.ts +19 -9
  291. package/src/apps/data/routes/balances.test.ts +148 -11
  292. package/src/apps/data/routes/balances.ts +140 -51
  293. package/src/apps/data/routes/exchanges.test.ts +544 -9
  294. package/src/apps/data/routes/exchanges.ts +690 -103
  295. package/src/apps/data/routes/fee-amm.test.ts +47 -23
  296. package/src/apps/data/routes/fee-amm.ts +98 -119
  297. package/src/apps/data/routes/funding.test.ts +113 -8
  298. package/src/apps/data/routes/funding.ts +77 -51
  299. package/src/apps/data/routes/receipts.test.ts +129 -23
  300. package/src/apps/data/routes/receipts.ts +205 -100
  301. package/src/apps/data/routes/tokens.test.ts +43 -0
  302. package/src/apps/data/routes/tokens.ts +28 -13
  303. package/src/apps/data/routes/transactions.test.ts +72 -19
  304. package/src/apps/data/routes/transactions.ts +162 -92
  305. package/src/apps/data/routes/transfers.test.ts +341 -31
  306. package/src/apps/data/routes/transfers.ts +358 -158
  307. package/src/apps/data/routes/valuation.test.ts +437 -0
  308. package/src/apps/data/routes/valuation.ts +423 -0
  309. package/src/apps/data/routes/verified-tokens.ts +14 -2
  310. package/src/apps/data/routes/webhooks.test.ts +56 -8
  311. package/src/apps/data/routes/webhooks.ts +144 -27
  312. package/src/apps/index.ts +2 -1
  313. package/src/apps/management/App.ts +13 -0
  314. package/src/apps/management/routes/api-keys.test.ts +14 -0
  315. package/src/apps/management/routes/api-keys.ts +27 -3
  316. package/src/apps/management/routes/billing.test.ts +152 -20
  317. package/src/apps/management/routes/billing.ts +34 -18
  318. package/src/apps/management/routes/faucet.test.ts +87 -0
  319. package/src/apps/management/routes/faucet.ts +201 -0
  320. package/src/apps/management/routes/invite-links.test.ts +117 -0
  321. package/src/apps/management/routes/invite-links.ts +43 -1
  322. package/src/apps/management/routes/me.test.ts +9 -99
  323. package/src/apps/management/routes/orgs.test.ts +45 -2
  324. package/src/apps/management/routes/orgs.ts +6 -0
  325. package/src/apps/management/routes/scopes.test.ts +1 -0
  326. package/src/apps/management/routes/usage.test.ts +6 -0
  327. package/src/apps/management/routes/usage.ts +7 -0
  328. package/src/apps/management/routes/verified-token-requests.test.ts +377 -0
  329. package/src/apps/management/routes/verified-token-requests.ts +397 -0
  330. package/src/apps/management/routes/webhooks.test.ts +426 -0
  331. package/src/apps/management/routes/webhooks.ts +672 -0
  332. package/src/apps/mpp/App.ts +86 -0
  333. package/src/apps/mpp/Mpp.test.ts +193 -0
  334. package/src/apps/mpp/Mpp.ts +353 -0
  335. package/src/apps/mpp/index.ts +2 -0
  336. package/src/apps/mpp/internal/Adapter.ts +11 -0
  337. package/src/apps/mpp/internal/Error.ts +9 -0
  338. package/src/apps/mpp/internal/Idempotency.ts +7 -0
  339. package/src/apps/mpp/internal/Screening.ts +10 -0
  340. package/src/apps/relay/App.test.ts +88 -6
  341. package/src/apps/relay/App.ts +61 -9
  342. package/src/db/Db.test.ts +19 -0
  343. package/src/db/Db.ts +125 -0
  344. package/src/db/Schema.ts +68 -0
  345. package/src/db/tables/earlyAccess.ts +86 -0
  346. package/src/db/tables/enabledBillingSources.test.ts +303 -0
  347. package/src/db/tables/enabledBillingSources.ts +118 -0
  348. package/src/db/tables/invitations.ts +3 -0
  349. package/src/db/tables/inviteLinks.ts +22 -3
  350. package/src/db/tables/organizations.test.ts +103 -0
  351. package/src/db/tables/organizations.ts +18 -2
  352. package/src/db/tables/sponsoredTransactions.ts +4 -0
  353. package/src/db/tables/verifiedTokenRequests.test.ts +200 -0
  354. package/src/db/tables/verifiedTokenRequests.ts +182 -0
  355. package/src/db/tables/webhookSubscriptions.ts +31 -0
  356. package/src/handlers/internal/utils.ts +22 -6
  357. package/src/handlers/relay.errors.test.ts +153 -0
  358. package/src/handlers/relay.ts +42 -7
  359. package/src/internal/Auth.test.ts +3 -3
  360. package/src/internal/Auth.ts +15 -47
  361. package/src/internal/FundingQuotes.test.ts +161 -6
  362. package/src/internal/FundingQuotes.ts +141 -39
  363. package/src/internal/Log.test-d.ts +21 -0
  364. package/src/internal/Log.test.ts +141 -0
  365. package/src/internal/Log.ts +93 -6
  366. package/src/internal/MetricSink.test.ts +215 -0
  367. package/src/internal/MetricSink.ts +24 -0
  368. package/src/internal/OpenApi.ts +3 -0
  369. package/src/internal/Response.test.ts +28 -0
  370. package/src/internal/Response.ts +12 -4
  371. package/src/internal/Schema.ts +50 -5
  372. package/src/internal/Value.test.ts +20 -0
  373. package/src/internal/Value.ts +24 -1
  374. package/src/internal/VerifiedTokens.ts +9 -1
  375. package/src/internal/Webhooks.test.ts +140 -0
  376. package/src/internal/Webhooks.ts +53 -9
@@ -17,7 +17,9 @@ import * as Timing from '../../../internal/Timing.js'
17
17
  import * as Ttl from '../../../internal/Ttl.js'
18
18
  import * as Value from '../../../internal/Value.js'
19
19
  import * as VerifiedTokens from '../../../internal/VerifiedTokens.js'
20
+ import * as FxOracle from '../FxOracle.js'
20
21
  import * as Tokens from './tokens.js'
22
+ import * as Valuation from './valuation.js'
21
23
 
22
24
  // TIP-20 tokens emit the standard ERC-20 Transfer event. Supplying the
23
25
  // signature exposes a decoded `Transfer` CTE (with `from`/`to`/`value`) to
@@ -31,37 +33,16 @@ export const signature = 'event Transfer(address indexed from, address indexed t
31
33
  const memoSignature =
32
34
  'event TransferWithMemo(address indexed from, address indexed to, uint256 amount, bytes32 indexed memo)'
33
35
 
36
+ const tokenFields = ['token.logoUri', 'token.verified'] as const
37
+
34
38
  /** Zod schemas owned by the transfer handlers. */
35
39
  export namespace schema {
36
- /**
37
- * The token moved by a transfer: its contract address and the transferred
38
- * amount, plus the trimmed token reference spread in when requested via
39
- * `include=token`. Reference fields are absent on the base (no-`include`)
40
- * response and best-effort when requested (a row whose metadata is
41
- * unavailable keeps just `address`/`amount`). Shared by every per-row
42
- * transfer response across the API.
43
- *
44
- * The optional metadata fields are inlined here (rather than spread from
45
- * `Tokens.schema.Token` via `z.partial`) so this module avoids referencing
46
- * the `tokens` schema namespace at module-init time — `transfers` and
47
- * `tokens` import each other for runtime helpers (`Transfers.query`,
48
- * `Tokens.resolveToken`), and the schema-evaluation back-edge would
49
- * otherwise deadlock the namespace.
50
- */
40
+ /** A transfer token reference with RPC metadata. Inlined to avoid the `transfers`/`tokens` module cycle. */
51
41
  export const SourceToken = z
52
42
  .object({
53
43
  address: Schema.TokenAddress.check(z.describe('The TIP-20 token contract address on Tempo.')),
54
- amount: z
55
- .string()
56
- .check(
57
- z.regex(/^\d+$/),
58
- z.describe(
59
- 'A non-negative whole number, given as a decimal string so very large token amounts keep full precision. Expressed in the smallest unit of the token.',
60
- ),
61
- z.meta({ examples: ['1000000'] }),
62
- ),
63
44
  currency: z
64
- .optional(z.string())
45
+ .string()
65
46
  .check(
66
47
  z.describe(
67
48
  'The currency label for this token, such as `USD` for USD-denominated stablecoins.',
@@ -69,7 +50,8 @@ export namespace schema {
69
50
  z.meta({ examples: ['USD'] }),
70
51
  ),
71
52
  decimals: z
72
- .optional(z.number().check(z.int(), z.nonnegative()))
53
+ .number()
54
+ .check(z.int(), z.nonnegative())
73
55
  .check(
74
56
  z.describe(
75
57
  'The number of decimal places the token uses; Tempo stablecoins typically use 6.',
@@ -77,7 +59,7 @@ export namespace schema {
77
59
  z.meta({ examples: [6] }),
78
60
  ),
79
61
  logoUri: z.optional(z.string()).check(
80
- z.describe('A URL for the token’s logo image, when one is available.'),
62
+ z.describe('A URL for the token’s logo image, returned with `include=token.logoUri`.'),
81
63
  z.meta({
82
64
  examples: [
83
65
  'https://api.tempo.xyz/data/4217/icons/0x20c0000000000000000000008f5425160ebe5525.svg',
@@ -85,10 +67,10 @@ export namespace schema {
85
67
  }),
86
68
  ),
87
69
  name: z
88
- .optional(z.string())
70
+ .string()
89
71
  .check(z.describe('The token’s human-readable name.'), z.meta({ examples: ['USD Coin'] })),
90
72
  symbol: z
91
- .optional(z.string())
73
+ .string()
92
74
  .check(
93
75
  z.describe('The short ticker symbol wallets and apps show for this token.'),
94
76
  z.meta({ examples: ['USDC'] }),
@@ -96,16 +78,30 @@ export namespace schema {
96
78
  verified: z
97
79
  .optional(z.boolean())
98
80
  .check(
99
- z.describe('Whether this token is in Tempo’s curated verified token list.'),
81
+ z.describe(
82
+ 'Whether this token is in Tempo’s curated verified list, returned with `include=token.verified`.',
83
+ ),
100
84
  z.meta({ examples: [true] }),
101
85
  ),
102
86
  })
103
87
  .check(
104
88
  z.describe(
105
- 'The token and amount that moved in this transfer, with token metadata added when you request `include=token`.',
89
+ 'The token that moved in this transfer, with RPC metadata and optional curated fields.',
106
90
  ),
107
91
  )
108
92
 
93
+ /** A token amount carrying its nominal value in the requested denomination. */
94
+ const ValuedAmount = z.extend(Schema.TokenAmount, {
95
+ valuation: z
96
+ .optional(z.nullable(Valuation.schema.Value))
97
+ .check(
98
+ z.describe(
99
+ 'The amount’s nominal value in the requested `denomination`. `null` when the token is ' +
100
+ 'unverified, its display currency has no rate, or rates were unavailable.',
101
+ ),
102
+ ),
103
+ })
104
+
109
105
  /**
110
106
  * Filter predicates shared verbatim by the `GET /transfers` list query
111
107
  * ({@link getTransfers.Query}, which spreads `Predicates.shape`) and `transfer`
@@ -137,18 +133,17 @@ export namespace schema {
137
133
  export namespace getTransfers {
138
134
  /** Optional related resources that callers opt into via `include`. */
139
135
  export const Include = z
140
- .enum(['crossToken', 'memo', 'token', 'totalCount'])
136
+ .enum(['crossToken', 'memo', ...tokenFields, 'totalCount'])
141
137
  .check(z.describe('Related resources to include only when you request them with `include`.'))
142
138
 
143
139
  /**
144
140
  * Parses a comma-separated `include` query value into a list of optional
145
- * resources to embed. Extra lookups (token metadata, the `TransferWithMemo`
146
- * memo/attribution decode, the capped total count) each run only when their
147
- * resource is requested, keeping the base transfer page fast.
141
+ * resources to embed. Extra lookups run only when their resource is
142
+ * requested, keeping the base transfer page fast.
148
143
  */
149
144
  export const includeQuery = Schema.includeQuery(
150
145
  Include,
151
- 'Comma-separated related resources to include, such as `token,memo,crossToken,totalCount`.',
146
+ 'Comma-separated related resources to include, such as `token.logoUri,token.verified,memo,crossToken,totalCount`.',
152
147
  )
153
148
 
154
149
  /**
@@ -174,6 +169,7 @@ export namespace schema {
174
169
  'blockNumber.to': Schema.blockNumberBound('transfers', 'to'),
175
170
  chainId: Schema.ChainIdQuery,
176
171
  cursor: Schema.Cursor,
172
+ denomination: Schema.Denomination,
177
173
  include: includeQuery,
178
174
  limit: Schema.Limit,
179
175
  order: Schema.Order,
@@ -214,11 +210,18 @@ export namespace schema {
214
210
  z.describe('The block number where this transfer was recorded.'),
215
211
  z.meta({ examples: [23456789] }),
216
212
  ),
213
+ destinationAmount: z
214
+ .optional(ValuedAmount)
215
+ .check(
216
+ z.describe(
217
+ 'Amount delivered to the recipient for a cross-token transfer. Omitted with `destinationToken` for a same-token transfer.',
218
+ ),
219
+ ),
217
220
  destinationToken: z
218
221
  .optional(schema.SourceToken)
219
222
  .check(
220
223
  z.describe(
221
- 'The token and amount delivered to the recipient when it differs from `sourceToken` — a cross-token transfer routed through the DEX, where `sourceToken` is the token swapped in. Omitted for same-token transfers, and only returned when you request `include=crossToken`.',
224
+ 'The token delivered to the recipient when it differs from `sourceToken`. Omitted for same-token transfers and returned only with `include=crossToken`.',
222
225
  ),
223
226
  ),
224
227
  id: z.string().check(
@@ -249,7 +252,8 @@ export namespace schema {
249
252
  z.describe('The account address that received the tokens.'),
250
253
  ),
251
254
  sender: Schema.Address.check(z.describe('The account address that sent the tokens.')),
252
- sourceToken: schema.SourceToken.check(z.describe('The token and amount that moved.')),
255
+ sourceAmount: ValuedAmount.check(z.describe('Amount associated with `sourceToken`.')),
256
+ sourceToken: schema.SourceToken.check(z.describe('The token that moved.')),
253
257
  timestamp: z.iso
254
258
  .datetime()
255
259
  .check(
@@ -261,29 +265,62 @@ export namespace schema {
261
265
  ),
262
266
  })
263
267
  .check(
268
+ z.refine(
269
+ (data) =>
270
+ (data.destinationToken === undefined) === (data.destinationAmount === undefined),
271
+ {
272
+ error: '`destinationAmount` and `destinationToken` must be returned together.',
273
+ path: ['destinationAmount'],
274
+ },
275
+ ),
264
276
  z.describe(
265
277
  'One TIP-20 token transfer event, optionally including a payment memo for reconciliation or invoices.',
266
278
  ),
267
279
  )
268
280
 
281
+ /** Page-level resources: opt-in counts plus valuation rate provenance. */
282
+ export const Meta = z
283
+ .object({
284
+ pricing: z
285
+ .optional(Valuation.schema.Pricing)
286
+ .check(
287
+ z.describe(
288
+ 'Rate provenance for leg valuations. Present when conversion rates were ' +
289
+ 'consulted; absent when every value was identity-valued or rates were unavailable.',
290
+ ),
291
+ ),
292
+ totalCount: z.optional(Schema.TotalCount),
293
+ totalCountCapped: z.optional(Schema.TotalCountCapped),
294
+ })
295
+ .check(z.describe('Page-level resources attached to this response.'))
296
+
269
297
  /** Page of TIP-20 token transfers across the chain, ordered by block then log index. */
270
298
  export const Response = z
271
299
  .object({
272
300
  data: z.array(Transfer).check(z.describe('The transfers in this page.')),
273
301
  // Response-wide `meta` carries the opt-in capped total count
274
- // (`include=totalCount`); per-row embeds (`memo`, `token`) attach to
275
- // each `data` item, not here.
302
+ // (`include=totalCount`) and valuation rate provenance; per-row embeds
303
+ // (`memo`, `token`) attach to each `data` item, not here.
276
304
  meta: z
277
- .optional(Schema.CountMeta)
278
- .check(z.describe('Page-level resources you requested with `include`.')),
305
+ .optional(Meta)
306
+ .check(z.describe('Page-level resources: opt-in counts and valuation rate provenance.')),
279
307
  nextCursor: Schema.NextCursor,
280
308
  })
281
309
  .check(z.describe('A page of TIP-20 token transfer events across the chain.'))
282
310
  }
283
311
  }
284
312
 
313
+ type UnenrichedTransfer = Omit<
314
+ z.output<typeof schema.getTransfers.Transfer>,
315
+ 'destinationAmount' | 'destinationToken' | 'sourceAmount' | 'sourceToken'
316
+ > & {
317
+ destinationToken?: CrossToken | undefined
318
+ sourceToken: CrossToken
319
+ }
320
+
285
321
  /** Creates transfer handlers. */
286
- export function transfers() {
322
+ export function transfers(options: transfers.Options = {}) {
323
+ const oracle = options.fx?.oracle ?? FxOracle.ecb()
287
324
  return new Hono<App.Environment>().get(
288
325
  '/v1/transfers',
289
326
  Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }),
@@ -295,7 +332,7 @@ export function transfers() {
295
332
  description: 'List token transfer events across Tempo.',
296
333
  operationId: 'getTransfers',
297
334
  responses: OpenApi.responses({
298
- errors: { 502: 'The upstream indexer could not serve this request.' },
335
+ errors: { 502: 'The indexer or Tempo RPC could not serve this request.' },
299
336
  success: {
300
337
  description: 'A page of token transfer events.',
301
338
  schema: schema.getTransfers.Response,
@@ -306,7 +343,7 @@ export function transfers() {
306
343
  }),
307
344
  Cache.response<App.Environment>({
308
345
  cacheControl: Cache.policies.feed,
309
- name: 'tempo-api:transfers:v1',
346
+ name: 'tempo-api:transfers:v2',
310
347
  key: (c) => Cache.urlKey(c, schema.getTransfers.Query),
311
348
  }),
312
349
  async (c) => {
@@ -319,11 +356,10 @@ export function transfers() {
319
356
 
320
357
  const options = c.req.valid('query')
321
358
  const chainId = options.chainId ?? c.get('chainId')
359
+ const denomination = options.denomination ?? 'USD'
322
360
 
323
- // Cursor pages are anchored below the head and effectively immutable,
324
- // so cache them aggressively; the head page keeps the route's short
325
- // `feed` default.
326
- if (options.cursor !== undefined) Cache.setPolicy(c, Cache.policies.immutable)
361
+ // Cursor pages carry current valuations and curated verification data.
362
+ if (options.cursor !== undefined) Cache.setPolicy(c, Cache.policies.metadata)
327
363
 
328
364
  // Filters shared by the page query and the opt-in total count, so the
329
365
  // count always matches the page it annotates.
@@ -361,12 +397,7 @@ export function transfers() {
361
397
  timing: 'transfers',
362
398
  })
363
399
 
364
- // Token metadata is opt-in. When requested, resolve each page token's
365
- // metadata concurrently (sharing one verified-token fetch) and spread
366
- // it into the row's `sourceToken`. Resolution is best-effort: a token
367
- // whose metadata is unavailable keeps `sourceToken` as just
368
- // `address`/`amount` on that row rather than failing the whole page.
369
- let data: z.output<typeof schema.getTransfers.Transfer>[] = page.data.map((row) => ({
400
+ let data: UnenrichedTransfer[] = page.data.map((row) => ({
370
401
  blockNumber: row.blockNumber,
371
402
  id: `${row.transactionHash}-${row.logIndex}`,
372
403
  logIndex: row.logIndex,
@@ -427,73 +458,157 @@ export function transfers() {
427
458
  return suppress.has(key) ? [] : [transfer]
428
459
  })
429
460
  }
430
- if (options.include.includes('token') && data.length > 0) {
431
- const uniqueTokens = Array.from(
432
- new Set(
433
- data.flatMap((transfer) => [
434
- transfer.sourceToken.address,
435
- ...(transfer.destinationToken ? [transfer.destinationToken.address] : []),
436
- ]),
437
- ),
438
- )
439
- const snapshot = await VerifiedTokens.snapshot(c, chainId)
440
- const resolved = await Timing.time(c, 'tokens', () =>
461
+ // Verification is ancillary to the feed. Skip empty pages and degrade
462
+ // cold snapshot failures to null valuations.
463
+ const snapshot =
464
+ data.length > 0
465
+ ? await VerifiedTokens.snapshot(c, chainId).catch(() => undefined)
466
+ : undefined
467
+ const includeLogoUri = options.include.includes('token.logoUri')
468
+ const includeVerified = options.include.includes('token.verified')
469
+ const uniqueTokens = Array.from(
470
+ new Set(
471
+ data.flatMap((transfer) => [
472
+ transfer.sourceToken.address,
473
+ ...(transfer.destinationToken ? [transfer.destinationToken.address] : []),
474
+ ]),
475
+ ),
476
+ )
477
+ const tokensByAddress = new Map(
478
+ await Timing.time(c, 'tokens', () =>
441
479
  Promise.all(
442
- uniqueTokens.map((token) =>
443
- Tokens.resolveToken(c, {
444
- address: token,
445
- chainId,
446
- snapshot,
447
- })
448
- .then((metadata) => [token, metadata] as const)
449
- .catch(() => [token, undefined] as const),
450
- ),
480
+ uniqueTokens.map(async (token) => {
481
+ const [logoUri, metadata] = await Promise.all([
482
+ includeLogoUri
483
+ ? Timing.time(c, 'token_logo', () =>
484
+ Tokens.getTokenLogo(c, { address: token, chainId }),
485
+ ).catch(() => undefined)
486
+ : undefined,
487
+ Timing.time(c, 'token_metadata', () =>
488
+ Tokens.getTokenMetadata(c, { address: token, chainId }),
489
+ ),
490
+ ])
491
+ return [
492
+ token.toLowerCase(),
493
+ {
494
+ ...metadata,
495
+ logoUri: includeLogoUri
496
+ ? (logoUri ??
497
+ snapshot?.byAddress.get(token.toLowerCase())?.logoUri ??
498
+ metadata.logoUri)
499
+ : undefined,
500
+ },
501
+ ] as const
502
+ }),
451
503
  ),
452
- )
453
- const tokensByAddress = new Map(resolved)
454
- const withToken = <T extends { address: z.output<typeof Schema.TokenAddress> }>(
455
- entry: T,
456
- ) => {
457
- const token = tokensByAddress.get(entry.address)
458
- return token ? { ...entry, ...token } : entry
504
+ ),
505
+ )
506
+
507
+ // Value each leg via its curated display currency, in the requested
508
+ // denomination.
509
+ const rates = await Valuation.ratesFor(c, {
510
+ currencies: data.flatMap((transfer) =>
511
+ [
512
+ transfer.sourceToken.address,
513
+ ...(transfer.destinationToken ? [transfer.destinationToken.address] : []),
514
+ ].flatMap((address) => {
515
+ const held = snapshot?.byAddress.get(address.toLowerCase())?.currency
516
+ return held === undefined ? [] : [held]
517
+ }),
518
+ ),
519
+ denomination,
520
+ oracle,
521
+ })
522
+ const enrich = (leg: CrossToken) => {
523
+ const metadata = tokensByAddress.get(leg.address.toLowerCase())
524
+ if (!metadata) throw new Error(`Token metadata unavailable for ${leg.address}`)
525
+ return {
526
+ amount: {
527
+ ...Value.tokenAmount({
528
+ baseUnits: leg.amount,
529
+ currency: metadata.currency,
530
+ decimals: metadata.decimals,
531
+ }),
532
+ valuation: Valuation.valuationFor({
533
+ amount: BigInt(leg.amount),
534
+ denomination,
535
+ rates,
536
+ token: snapshot?.byAddress.get(leg.address.toLowerCase()),
537
+ }),
538
+ },
539
+ token: {
540
+ address: leg.address,
541
+ currency: metadata.currency,
542
+ decimals: metadata.decimals,
543
+ ...(metadata.logoUri !== undefined ? { logoUri: metadata.logoUri } : {}),
544
+ name: metadata.name,
545
+ symbol: metadata.symbol,
546
+ ...(includeVerified && snapshot
547
+ ? { verified: snapshot.byAddress.has(leg.address.toLowerCase()) }
548
+ : {}),
549
+ },
459
550
  }
460
- data = data.map((transfer) => ({
461
- ...transfer,
462
- sourceToken: withToken(transfer.sourceToken),
463
- ...(transfer.destinationToken
464
- ? { destinationToken: withToken(transfer.destinationToken) }
465
- : {}),
466
- }))
467
551
  }
552
+ const responseData: z.output<typeof schema.getTransfers.Transfer>[] = data.map(
553
+ ({ destinationToken, sourceToken, ...transfer }) => {
554
+ const source = enrich(sourceToken)
555
+ if (!destinationToken)
556
+ return {
557
+ ...transfer,
558
+ sourceAmount: source.amount,
559
+ sourceToken: source.token,
560
+ }
561
+ const destination = enrich(destinationToken)
562
+ return {
563
+ ...transfer,
564
+ destinationAmount: destination.amount,
565
+ destinationToken: destination.token,
566
+ sourceAmount: source.amount,
567
+ sourceToken: source.token,
568
+ }
569
+ },
570
+ )
468
571
 
469
- const meta = countPromise ? await countPromise : undefined
572
+ const counts = countPromise ? await countPromise : undefined
573
+ const meta = {
574
+ ...(rates ? { pricing: Valuation.pricing(rates, oracle) } : {}),
575
+ ...counts,
576
+ }
470
577
 
471
578
  return c.json(
472
579
  Response.validated(schema.getTransfers.Response, {
473
- data,
474
- ...(meta ? { meta } : {}),
580
+ data: responseData,
581
+ ...(Object.keys(meta).length > 0 ? { meta } : {}),
475
582
  nextCursor: page.nextCursor,
476
583
  }),
477
584
  200,
478
585
  )
479
586
  } catch (cause) {
587
+ if (cause instanceof Valuation.UnsupportedDenominationError)
588
+ return Response.error(c, { code: 'query_invalid', message: cause.message, status: 400 })
480
589
  return Response.upstream(c, cause)
481
590
  }
482
591
  },
483
592
  )
484
593
  }
485
594
 
595
+ export declare namespace transfers {
596
+ /** Options for the transfer handlers. */
597
+ type Options = {
598
+ /** FX configuration backing leg valuation. */
599
+ fx?: Valuation.addresses.Fx | undefined
600
+ }
601
+ }
602
+
486
603
  /**
487
604
  * Shared TIP-20 `Transfer` page query. Used by `GET /transfers` and the
488
- * webhook poller. Builds the signature-decoded `Transfer` CTE query, applies
605
+ * webhook poller. Builds signature-decoded `Transfer` CTE queries, applies
489
606
  * opaque keyset pagination on `(block_num, log_idx)`, and decodes rows into a
490
607
  * strongly-typed intermediate shape that each caller maps to its own public
491
608
  * response.
492
609
  *
493
- * `eitherSide` (the subject address must appear on _either_ side of the
494
- * transfer) is exposed alongside the one-sided `sender`/`recipient` filters
495
- * because composing it with the keyset cursor pre-WHERE in SQL is awkward at
496
- * the call site; the helper takes the knob directly.
610
+ * `eitherSide` is exposed alongside one-sided `sender`/`recipient` filters
611
+ * because it requires two upstream scans. The helper owns their ordered merge.
497
612
  *
498
613
  * Memoization lives inside the helper under one canonical `transfers:v1:…`
499
614
  * key derived from every filter — so equivalent filter combinations share a
@@ -583,11 +698,11 @@ export async function scan(deps: scan.Deps, options: scan.Options): Promise<scan
583
698
  // translate to a SQL OFFSET of `(page - 1) * limit` rows.
584
699
  const offset =
585
700
  options.page !== undefined && options.page > 1 ? (options.page - 1) * limit : undefined
701
+ const address = options.eitherSide
586
702
 
587
- // Transfer events filtered by any combination of contract / subject /
588
- // sender / recipient / block / timestamp. Ordered by block then log
589
- // index so transfers within a block stay stable. The keyset cursor clause is
590
- // page-only and added on top of the shared filters below.
703
+ // Transfer events filtered by any combination of contract / sender /
704
+ // recipient / block / timestamp. The address shortcut is split below so
705
+ // each upstream query can use one indexed side.
591
706
  const filters = filterClauses(options)
592
707
  if (cursor !== undefined)
593
708
  filters.push(
@@ -596,24 +711,41 @@ export async function scan(deps: scan.Deps, options: scan.Options): Promise<scan
596
711
  { literal: Cursor.literal(cursor[1]!, 'int'), name: 'log_idx', order },
597
712
  ]),
598
713
  )
599
- const where = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''
600
-
601
- // Fetch one extra row to detect `hasMore` without a separate count query.
602
- const result = await tidx.fetch({
603
- chainId,
604
- ...(tokenScoped ? { engine: 'clickhouse' } : { signatures: [signature] }),
605
- query: `
606
- SELECT "from", "to", ${tokenScoped ? 'token AS address, amount AS value' : 'address, value'}, tx_hash, block_num, log_idx, block_timestamp
607
- FROM ${tokenScoped ? 'token_transfers' : 'Transfer'}
608
- ${where}
609
- ORDER BY block_num ${direction}, log_idx ${direction}
610
- LIMIT ${limit + 1}${offset !== undefined ? ` OFFSET ${offset}` : ''}
611
- ` as string,
612
- })
714
+ const addressScoped = address !== undefined
715
+ const window = addressScoped ? (offset ?? 0) + limit + 1 : limit + 1
716
+ const fetchRows = async (side?: 'from' | 'to') => {
717
+ const sideFilters =
718
+ side !== undefined && address !== undefined
719
+ ? [`"${side}" = '${address}'`, ...filters]
720
+ : filters
721
+ const where = sideFilters.length > 0 ? `WHERE ${sideFilters.join(' AND ')}` : ''
722
+ const result = await tidx.fetch({
723
+ chainId,
724
+ ...(tokenScoped ? { engine: 'clickhouse' } : { signatures: [signature] }),
725
+ query: `
726
+ SELECT "from", "to", ${tokenScoped ? 'token AS address, amount AS value' : 'address, value'}, tx_hash, block_num, log_idx, block_timestamp
727
+ FROM ${tokenScoped ? 'token_transfers' : 'Transfer'}
728
+ ${where}
729
+ ORDER BY block_num ${direction}, log_idx ${direction}
730
+ LIMIT ${window}${!addressScoped && offset !== undefined ? ` OFFSET ${offset}` : ''}
731
+ ` as string,
732
+ })
733
+ return result.rows
734
+ }
735
+
736
+ // TIDX cannot plan `(from = X OR to = X)` over the decoded Transfer table.
737
+ // Merge two bounded, server-filtered streams while preserving global order.
738
+ const rows = addressScoped
739
+ ? mergeTransferRows((await Promise.all([fetchRows('from'), fetchRows('to')])).flat(), {
740
+ limit: limit + 1,
741
+ offset: offset ?? 0,
742
+ order,
743
+ })
744
+ : await fetchRows()
613
745
 
614
746
  // The next page anchors below the last fetched row's `(block, log_idx)`.
615
747
  const page = Cursor.paginate({
616
- rows: result.rows,
748
+ rows,
617
749
  limit,
618
750
  key: (row) => {
619
751
  const block = Value.toNumber(row['block_num'])
@@ -660,6 +792,49 @@ export async function scan(deps: scan.Deps, options: scan.Options): Promise<scan
660
792
  return { hasMore: page.hasMore, items, limit, nextCursor: page.nextCursor }
661
793
  }
662
794
 
795
+ /** Deduplicates and globally orders bounded `from`/`to` transfer streams. */
796
+ function mergeTransferRows(
797
+ rows: readonly Record<string, unknown>[],
798
+ options: mergeTransferRows.Options,
799
+ ): Record<string, unknown>[] {
800
+ const positioned: mergeTransferRows.PositionedRow[] = []
801
+ const seen = new Set<string>()
802
+ for (const row of rows) {
803
+ const blockNumber = Value.toNumber(row['block_num'])
804
+ const logIndex = Value.toNumber(row['log_idx'])
805
+ const transactionHash = Value.toText(row['tx_hash'])
806
+ if (blockNumber === undefined || logIndex === undefined || !transactionHash) continue
807
+ const key = eventKey(transactionHash, logIndex)
808
+ if (seen.has(key)) continue
809
+ seen.add(key)
810
+ positioned.push({ blockNumber, logIndex, row })
811
+ }
812
+ positioned.sort((a, b) => {
813
+ const comparison = a.blockNumber - b.blockNumber || a.logIndex - b.logIndex
814
+ return options.order === 'asc' ? comparison : -comparison
815
+ })
816
+ return positioned.slice(options.offset, options.offset + options.limit).map(({ row }) => row)
817
+ }
818
+
819
+ declare namespace mergeTransferRows {
820
+ type Options = {
821
+ /** Maximum merged rows to return. */
822
+ limit: number
823
+ /** Number of globally ordered rows to skip. */
824
+ offset: number
825
+ /** Global transfer ordering. */
826
+ order: 'asc' | 'desc'
827
+ }
828
+ type PositionedRow = {
829
+ /** Transfer block number. */
830
+ blockNumber: number
831
+ /** Transfer log index. */
832
+ logIndex: number
833
+ /** Raw TIDX row. */
834
+ row: Record<string, unknown>
835
+ }
836
+ }
837
+
663
838
  export declare namespace scan {
664
839
  /** Dependencies for {@link scan}. */
665
840
  type Deps = {
@@ -689,11 +864,9 @@ export declare namespace scan {
689
864
  * physical `token` column; unscoped queries omit it and use the `Transfer` CTE.
690
865
  */
691
866
  function filterClauses(options: filterClauses.Options): string[] {
692
- const { eitherSide, fromBlock, fromTimestamp, recipient, sender, toBlock, toTimestamp, token } =
693
- options
867
+ const { fromBlock, fromTimestamp, recipient, sender, toBlock, toTimestamp, token } = options
694
868
  const filters: string[] = []
695
869
  if (token !== undefined) filters.push(`token = '${token}'`)
696
- if (eitherSide !== undefined) filters.push(`("from" = '${eitherSide}' OR "to" = '${eitherSide}')`)
697
870
  if (sender !== undefined) filters.push(`"from" = '${sender}'`)
698
871
  if (recipient !== undefined) filters.push(`"to" = '${recipient}'`)
699
872
  if (fromBlock !== undefined) filters.push(`block_num >= ${fromBlock}`)
@@ -707,14 +880,7 @@ declare namespace filterClauses {
707
880
  /** The non-pagination filter fields of {@link scan.Options}. */
708
881
  type Options = Pick<
709
882
  scan.Options,
710
- | 'eitherSide'
711
- | 'fromBlock'
712
- | 'fromTimestamp'
713
- | 'recipient'
714
- | 'sender'
715
- | 'toBlock'
716
- | 'toTimestamp'
717
- | 'token'
883
+ 'fromBlock' | 'fromTimestamp' | 'recipient' | 'sender' | 'toBlock' | 'toTimestamp' | 'token'
718
884
  >
719
885
  }
720
886
 
@@ -763,7 +929,7 @@ export declare namespace count {
763
929
  /**
764
930
  * Context-free core of {@link count}: counts token-scoped rows from
765
931
  * `token_transfers`, otherwise the decoded `Transfer` CTE. The capped inner
766
- * scan bounds work; the SQL aggregate runs on ClickHouse.
932
+ * scan bounds work while preserving each source's established query engine.
767
933
  */
768
934
  export async function countScan(
769
935
  deps: scan.Deps,
@@ -773,56 +939,90 @@ export async function countScan(
773
939
  const { tidx } = deps
774
940
  const filters = filterClauses(options)
775
941
  const tokenScoped = options.token !== undefined
776
- const where = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''
942
+ const address = options.eitherSide
777
943
 
778
944
  // Folded count: cross-token transfers collapse several `Transfer` legs into
779
945
  // one row, so a raw `count(*)` would overcount. Scan the capped `Transfer`
780
946
  // rows, classify each transaction, and subtract the folded-away legs. Bounded
781
947
  // by `countCap` like the raw count, so it stays within the indexer's budget.
782
948
  if (options.crossToken) {
949
+ const fetchRows = async (side?: 'from' | 'to') => {
950
+ const sideFilters =
951
+ side !== undefined && address !== undefined
952
+ ? [`"${side}" = '${address}'`, ...filters]
953
+ : filters
954
+ const sideWhere = sideFilters.length > 0 ? `WHERE ${sideFilters.join(' AND ')}` : ''
955
+ const result = await tidx.fetch({
956
+ chainId,
957
+ ...(tokenScoped ? { engine: 'clickhouse' } : { signatures: [signature] }),
958
+ query: `
959
+ SELECT tx_hash, block_num, log_idx, "from", "to", ${tokenScoped ? 'token AS address, amount AS value' : 'address, value'}
960
+ FROM ${tokenScoped ? 'token_transfers' : 'Transfer'}
961
+ ${sideWhere}
962
+ ORDER BY block_num, log_idx
963
+ LIMIT ${Schema.countCap}
964
+ ` as string,
965
+ })
966
+ return result.rows
967
+ }
968
+ const rows =
969
+ address !== undefined
970
+ ? mergeTransferRows((await Promise.all([fetchRows('from'), fetchRows('to')])).flat(), {
971
+ limit: Schema.countCap,
972
+ offset: 0,
973
+ order: 'asc',
974
+ })
975
+ : await fetchRows()
976
+ const rawCount = rows.length
977
+ let suppressed = 0
978
+ for (const legs of legsByTransaction(rows).values()) {
979
+ const bundle = classifyCrossToken(legs)
980
+ if (bundle) suppressed += bundle.suppressLogIndexes.length
981
+ }
982
+ return { totalCountCapped: rawCount >= Schema.countCap, totalCount: rawCount - suppressed }
983
+ }
984
+
985
+ const countRows = async (additionalFilters: readonly string[] = []) => {
986
+ const countFilters = [...additionalFilters, ...filters]
987
+ const countWhere = countFilters.length > 0 ? `WHERE ${countFilters.join(' AND ')}` : ''
783
988
  const result = await tidx.fetch({
784
989
  chainId,
785
990
  ...(tokenScoped ? { engine: 'clickhouse' } : { signatures: [signature] }),
786
991
  query: `
787
- SELECT tx_hash, log_idx, "from", "to", ${tokenScoped ? 'token AS address, amount AS value' : 'address, value'}
788
- FROM ${tokenScoped ? 'token_transfers' : 'Transfer'}
789
- ${where}
790
- ORDER BY block_num, log_idx
791
- LIMIT ${Schema.countCap}
992
+ SELECT count(*) AS total
993
+ FROM (
994
+ SELECT 1
995
+ FROM ${tokenScoped ? 'token_transfers' : 'Transfer'}
996
+ ${countWhere}
997
+ LIMIT ${Schema.countCap}
998
+ ) AS capped
792
999
  ` as string,
793
1000
  })
794
- const rawCount = result.rows.length
795
- let suppressed = 0
796
- for (const legs of legsByTransaction(result.rows).values()) {
797
- const bundle = classifyCrossToken(legs)
798
- if (bundle) suppressed += bundle.suppressLogIndexes.length
799
- }
800
- return { totalCountCapped: rawCount >= Schema.countCap, totalCount: rawCount - suppressed }
1001
+ return Value.toNumber(result.rows[0]?.['total']) ?? 0
801
1002
  }
802
1003
 
803
- const result = await tidx.fetch({
804
- chainId,
805
- engine: 'clickhouse',
806
- ...(tokenScoped ? {} : { signatures: [signature] }),
807
- query: `
808
- SELECT count(*) AS total
809
- FROM (
810
- SELECT 1
811
- FROM ${tokenScoped ? 'token_transfers' : 'Transfer'}
812
- ${where}
813
- LIMIT ${Schema.countCap}
814
- ) AS capped
815
- ` as string,
816
- })
1004
+ if (address !== undefined) {
1005
+ // Keep branches disjoint so self-transfers count once.
1006
+ const [sent, received] = await Promise.all([
1007
+ countRows([`"from" = '${address}'`]),
1008
+ countRows([`"to" = '${address}'`, `"from" != '${address}'`]),
1009
+ ])
1010
+ const rawCount = sent + received
1011
+ return {
1012
+ totalCount: Math.min(rawCount, Schema.countCap),
1013
+ totalCountCapped:
1014
+ sent >= Schema.countCap || received >= Schema.countCap || rawCount >= Schema.countCap,
1015
+ }
1016
+ }
817
1017
 
818
- const totalCount = Value.toNumber(result.rows[0]?.['total']) ?? 0
1018
+ const totalCount = await countRows()
819
1019
  return { totalCountCapped: totalCount >= Schema.countCap, totalCount }
820
1020
  }
821
1021
 
822
1022
  export declare namespace countScan {
823
1023
  /** Options for {@link countScan}: the shared filters plus the target chain. */
824
1024
  type Options = filterClauses.Options &
825
- Pick<scan.Options, 'chainId'> & {
1025
+ Pick<scan.Options, 'chainId' | 'eitherSide'> & {
826
1026
  /** Count folded cross-token transfers instead of raw `Transfer` logs. */
827
1027
  crossToken?: boolean | undefined
828
1028
  }
@@ -906,7 +1106,7 @@ declare namespace memoMap {
906
1106
  }
907
1107
  }
908
1108
 
909
- /** A token's contract address and moved amount, matching `SourceToken`'s core. */
1109
+ /** A transfer leg before amount and token metadata are separated. */
910
1110
  type CrossToken = { address: z.output<typeof Schema.Address>; amount: string }
911
1111
 
912
1112
  /** A decoded `Transfer` leg used for cross-token classification. */