tapimo 0.0.0 → 0.0.2

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 (422) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +119 -0
  3. package/dist/ApiKey.d.ts +65 -0
  4. package/dist/ApiKey.d.ts.map +1 -0
  5. package/dist/ApiKey.js +105 -0
  6. package/dist/ApiKey.js.map +1 -0
  7. package/dist/ApiKeySource.d.ts +166 -0
  8. package/dist/ApiKeySource.d.ts.map +1 -0
  9. package/dist/ApiKeySource.js +234 -0
  10. package/dist/ApiKeySource.js.map +1 -0
  11. package/dist/App.d.ts +19317 -0
  12. package/dist/App.d.ts.map +1 -0
  13. package/dist/App.js +612 -0
  14. package/dist/App.js.map +1 -0
  15. package/dist/Assets.d.ts +44 -0
  16. package/dist/Assets.d.ts.map +1 -0
  17. package/dist/Assets.js +23 -0
  18. package/dist/Assets.js.map +1 -0
  19. package/dist/Auth.d.ts +2 -0
  20. package/dist/Auth.d.ts.map +1 -0
  21. package/dist/Auth.js +2 -0
  22. package/dist/Auth.js.map +1 -0
  23. package/dist/Cli.d.ts +21 -0
  24. package/dist/Cli.d.ts.map +1 -0
  25. package/dist/Cli.js +13 -0
  26. package/dist/Cli.js.map +1 -0
  27. package/dist/Client.d.ts +20 -0
  28. package/dist/Client.d.ts.map +1 -0
  29. package/dist/Client.js +24 -0
  30. package/dist/Client.js.map +1 -0
  31. package/dist/Metrics.d.ts +70 -0
  32. package/dist/Metrics.d.ts.map +1 -0
  33. package/dist/Metrics.js +91 -0
  34. package/dist/Metrics.js.map +1 -0
  35. package/dist/Scope.d.ts +35 -0
  36. package/dist/Scope.d.ts.map +1 -0
  37. package/dist/Scope.js +38 -0
  38. package/dist/Scope.js.map +1 -0
  39. package/dist/Webhooks.d.ts +79 -0
  40. package/dist/Webhooks.d.ts.map +1 -0
  41. package/dist/Webhooks.js +107 -0
  42. package/dist/Webhooks.js.map +1 -0
  43. package/dist/admin/Access.d.ts +41 -0
  44. package/dist/admin/Access.d.ts.map +1 -0
  45. package/dist/admin/Access.js +75 -0
  46. package/dist/admin/Access.js.map +1 -0
  47. package/dist/admin/App.d.ts +1221 -0
  48. package/dist/admin/App.d.ts.map +1 -0
  49. package/dist/admin/App.js +113 -0
  50. package/dist/admin/App.js.map +1 -0
  51. package/dist/admin/apps/api-keys.d.ts +209 -0
  52. package/dist/admin/apps/api-keys.d.ts.map +1 -0
  53. package/dist/admin/apps/api-keys.js +129 -0
  54. package/dist/admin/apps/api-keys.js.map +1 -0
  55. package/dist/admin/apps/scopes.d.ts +46 -0
  56. package/dist/admin/apps/scopes.d.ts.map +1 -0
  57. package/dist/admin/apps/scopes.js +41 -0
  58. package/dist/admin/apps/scopes.js.map +1 -0
  59. package/dist/admin/apps/verified-tokens.d.ts +1206 -0
  60. package/dist/admin/apps/verified-tokens.d.ts.map +1 -0
  61. package/dist/admin/apps/verified-tokens.js +485 -0
  62. package/dist/admin/apps/verified-tokens.js.map +1 -0
  63. package/dist/admin/index.d.ts +3 -0
  64. package/dist/admin/index.d.ts.map +1 -0
  65. package/dist/admin/index.js +3 -0
  66. package/dist/admin/index.js.map +1 -0
  67. package/dist/admin/ui.generated.d.ts +11 -0
  68. package/dist/admin/ui.generated.d.ts.map +1 -0
  69. package/dist/admin/ui.generated.js +43 -0
  70. package/dist/admin/ui.generated.js.map +1 -0
  71. package/dist/apps/activities.d.ts +18725 -0
  72. package/dist/apps/activities.d.ts.map +1 -0
  73. package/dist/apps/activities.js +3138 -0
  74. package/dist/apps/activities.js.map +1 -0
  75. package/dist/apps/addresses.d.ts +3662 -0
  76. package/dist/apps/addresses.d.ts.map +1 -0
  77. package/dist/apps/addresses.js +22 -0
  78. package/dist/apps/addresses.js.map +1 -0
  79. package/dist/apps/balances.d.ts +342 -0
  80. package/dist/apps/balances.d.ts.map +1 -0
  81. package/dist/apps/balances.js +393 -0
  82. package/dist/apps/balances.js.map +1 -0
  83. package/dist/apps/blocks.d.ts +931 -0
  84. package/dist/apps/blocks.d.ts.map +1 -0
  85. package/dist/apps/blocks.js +689 -0
  86. package/dist/apps/blocks.js.map +1 -0
  87. package/dist/apps/coingecko.d.ts +1053 -0
  88. package/dist/apps/coingecko.d.ts.map +1 -0
  89. package/dist/apps/coingecko.js +1015 -0
  90. package/dist/apps/coingecko.js.map +1 -0
  91. package/dist/apps/exchanges.d.ts +3000 -0
  92. package/dist/apps/exchanges.d.ts.map +1 -0
  93. package/dist/apps/exchanges.js +3570 -0
  94. package/dist/apps/exchanges.js.map +1 -0
  95. package/dist/apps/fee-amm.d.ts +640 -0
  96. package/dist/apps/fee-amm.d.ts.map +1 -0
  97. package/dist/apps/fee-amm.js +815 -0
  98. package/dist/apps/fee-amm.js.map +1 -0
  99. package/dist/apps/indexer.d.ts +116 -0
  100. package/dist/apps/indexer.d.ts.map +1 -0
  101. package/dist/apps/indexer.js +270 -0
  102. package/dist/apps/indexer.js.map +1 -0
  103. package/dist/apps/receipts.d.ts +1438 -0
  104. package/dist/apps/receipts.d.ts.map +1 -0
  105. package/dist/apps/receipts.js +874 -0
  106. package/dist/apps/receipts.js.map +1 -0
  107. package/dist/apps/rpc.d.ts +173 -0
  108. package/dist/apps/rpc.d.ts.map +1 -0
  109. package/dist/apps/rpc.js +191 -0
  110. package/dist/apps/rpc.js.map +1 -0
  111. package/dist/apps/tokenlist.d.ts +192 -0
  112. package/dist/apps/tokenlist.d.ts.map +1 -0
  113. package/dist/apps/tokenlist.js +153 -0
  114. package/dist/apps/tokenlist.js.map +1 -0
  115. package/dist/apps/tokens.d.ts +2246 -0
  116. package/dist/apps/tokens.d.ts.map +1 -0
  117. package/dist/apps/tokens.js +1789 -0
  118. package/dist/apps/tokens.js.map +1 -0
  119. package/dist/apps/transactions.d.ts +2327 -0
  120. package/dist/apps/transactions.d.ts.map +1 -0
  121. package/dist/apps/transactions.js +975 -0
  122. package/dist/apps/transactions.js.map +1 -0
  123. package/dist/apps/transfers.d.ts +589 -0
  124. package/dist/apps/transfers.d.ts.map +1 -0
  125. package/dist/apps/transfers.js +769 -0
  126. package/dist/apps/transfers.js.map +1 -0
  127. package/dist/apps/verified-tokens.d.ts +544 -0
  128. package/dist/apps/verified-tokens.d.ts.map +1 -0
  129. package/dist/apps/verified-tokens.js +234 -0
  130. package/dist/apps/verified-tokens.js.map +1 -0
  131. package/dist/apps/webhooks.d.ts +7684 -0
  132. package/dist/apps/webhooks.d.ts.map +1 -0
  133. package/dist/apps/webhooks.js +2112 -0
  134. package/dist/apps/webhooks.js.map +1 -0
  135. package/dist/cloudflare.d.ts +32 -0
  136. package/dist/cloudflare.d.ts.map +1 -0
  137. package/dist/cloudflare.js +45 -0
  138. package/dist/cloudflare.js.map +1 -0
  139. package/dist/index.d.ts +16 -0
  140. package/dist/index.d.ts.map +1 -0
  141. package/dist/index.js +16 -0
  142. package/dist/index.js.map +1 -0
  143. package/dist/internal/Auth.d.ts +298 -0
  144. package/dist/internal/Auth.d.ts.map +1 -0
  145. package/dist/internal/Auth.js +618 -0
  146. package/dist/internal/Auth.js.map +1 -0
  147. package/dist/internal/Cache.d.ts +74 -0
  148. package/dist/internal/Cache.d.ts.map +1 -0
  149. package/dist/internal/Cache.js +215 -0
  150. package/dist/internal/Cache.js.map +1 -0
  151. package/dist/internal/Cursor.d.ts +79 -0
  152. package/dist/internal/Cursor.d.ts.map +1 -0
  153. package/dist/internal/Cursor.js +116 -0
  154. package/dist/internal/Cursor.js.map +1 -0
  155. package/dist/internal/EdgeCache.d.ts +59 -0
  156. package/dist/internal/EdgeCache.d.ts.map +1 -0
  157. package/dist/internal/EdgeCache.js +174 -0
  158. package/dist/internal/EdgeCache.js.map +1 -0
  159. package/dist/internal/Log.d.ts +104 -0
  160. package/dist/internal/Log.d.ts.map +1 -0
  161. package/dist/internal/Log.js +118 -0
  162. package/dist/internal/Log.js.map +1 -0
  163. package/dist/internal/MetricSink.d.ts +65 -0
  164. package/dist/internal/MetricSink.d.ts.map +1 -0
  165. package/dist/internal/MetricSink.js +146 -0
  166. package/dist/internal/MetricSink.js.map +1 -0
  167. package/dist/internal/Mpp.d.ts +71 -0
  168. package/dist/internal/Mpp.d.ts.map +1 -0
  169. package/dist/internal/Mpp.js +105 -0
  170. package/dist/internal/Mpp.js.map +1 -0
  171. package/dist/internal/OpenApi.d.ts +313 -0
  172. package/dist/internal/OpenApi.d.ts.map +1 -0
  173. package/dist/internal/OpenApi.fonts.d.ts +2 -0
  174. package/dist/internal/OpenApi.fonts.d.ts.map +1 -0
  175. package/dist/internal/OpenApi.fonts.js +39 -0
  176. package/dist/internal/OpenApi.fonts.js.map +1 -0
  177. package/dist/internal/OpenApi.js +335 -0
  178. package/dist/internal/OpenApi.js.map +1 -0
  179. package/dist/internal/OpenApi.logo.d.ts +6 -0
  180. package/dist/internal/OpenApi.logo.d.ts.map +1 -0
  181. package/dist/internal/OpenApi.logo.js +19 -0
  182. package/dist/internal/OpenApi.logo.js.map +1 -0
  183. package/dist/internal/Path.d.ts +5 -0
  184. package/dist/internal/Path.d.ts.map +1 -0
  185. package/dist/internal/Path.js +16 -0
  186. package/dist/internal/Path.js.map +1 -0
  187. package/dist/internal/RateLimit.d.ts +60 -0
  188. package/dist/internal/RateLimit.d.ts.map +1 -0
  189. package/dist/internal/RateLimit.js +48 -0
  190. package/dist/internal/RateLimit.js.map +1 -0
  191. package/dist/internal/Response.d.ts +132 -0
  192. package/dist/internal/Response.d.ts.map +1 -0
  193. package/dist/internal/Response.js +72 -0
  194. package/dist/internal/Response.js.map +1 -0
  195. package/dist/internal/Schema.d.ts +257 -0
  196. package/dist/internal/Schema.d.ts.map +1 -0
  197. package/dist/internal/Schema.js +360 -0
  198. package/dist/internal/Schema.js.map +1 -0
  199. package/dist/internal/Store.d.ts +204 -0
  200. package/dist/internal/Store.d.ts.map +1 -0
  201. package/dist/internal/Store.js +291 -0
  202. package/dist/internal/Store.js.map +1 -0
  203. package/dist/internal/Tidx.d.ts +120 -0
  204. package/dist/internal/Tidx.d.ts.map +1 -0
  205. package/dist/internal/Tidx.js +156 -0
  206. package/dist/internal/Tidx.js.map +1 -0
  207. package/dist/internal/Timing.d.ts +22 -0
  208. package/dist/internal/Timing.d.ts.map +1 -0
  209. package/dist/internal/Timing.js +32 -0
  210. package/dist/internal/Timing.js.map +1 -0
  211. package/dist/internal/Ttl.d.ts +25 -0
  212. package/dist/internal/Ttl.d.ts.map +1 -0
  213. package/dist/internal/Ttl.js +49 -0
  214. package/dist/internal/Ttl.js.map +1 -0
  215. package/dist/internal/Value.d.ts +47 -0
  216. package/dist/internal/Value.d.ts.map +1 -0
  217. package/dist/internal/Value.js +122 -0
  218. package/dist/internal/Value.js.map +1 -0
  219. package/dist/internal/VerifiedTokens.d.ts +228 -0
  220. package/dist/internal/VerifiedTokens.d.ts.map +1 -0
  221. package/dist/internal/VerifiedTokens.js +450 -0
  222. package/dist/internal/VerifiedTokens.js.map +1 -0
  223. package/dist/internal/Viem.d.ts +2082 -0
  224. package/dist/internal/Viem.d.ts.map +1 -0
  225. package/dist/internal/Viem.js +63 -0
  226. package/dist/internal/Viem.js.map +1 -0
  227. package/dist/internal/WebhookDestination.d.ts +198 -0
  228. package/dist/internal/WebhookDestination.d.ts.map +1 -0
  229. package/dist/internal/WebhookDestination.js +170 -0
  230. package/dist/internal/WebhookDestination.js.map +1 -0
  231. package/dist/internal/Webhooks.d.ts +433 -0
  232. package/dist/internal/Webhooks.d.ts.map +1 -0
  233. package/dist/internal/Webhooks.js +489 -0
  234. package/dist/internal/Webhooks.js.map +1 -0
  235. package/dist/internal/webhookDestinations/betterStack.d.ts +26 -0
  236. package/dist/internal/webhookDestinations/betterStack.d.ts.map +1 -0
  237. package/dist/internal/webhookDestinations/betterStack.js +76 -0
  238. package/dist/internal/webhookDestinations/betterStack.js.map +1 -0
  239. package/dist/internal/webhookDestinations/slack.d.ts +59 -0
  240. package/dist/internal/webhookDestinations/slack.d.ts.map +1 -0
  241. package/dist/internal/webhookDestinations/slack.js +422 -0
  242. package/dist/internal/webhookDestinations/slack.js.map +1 -0
  243. package/dist/internal/webhookDestinations/url.d.ts +17 -0
  244. package/dist/internal/webhookDestinations/url.d.ts.map +1 -0
  245. package/dist/internal/webhookDestinations/url.js +130 -0
  246. package/dist/internal/webhookDestinations/url.js.map +1 -0
  247. package/dist/openrpc.json +5302 -0
  248. package/package.json +46 -4
  249. package/seeds/31318/icons/0x20c0000000000000000000000000000000000000.svg +1 -0
  250. package/seeds/31318/icons/0x20c0000000000000000000000000000000000001.svg +1 -0
  251. package/seeds/31318/icons/0x20c0000000000000000000000000000000000002.svg +1 -0
  252. package/seeds/31318/icons/0x20c0000000000000000000000000000000000003.svg +1 -0
  253. package/seeds/31318/verified-tokens.json +30 -0
  254. package/seeds/4217/icons/0x20c0000000000000000000000000000000000000.svg +25 -0
  255. package/seeds/4217/icons/0x20c000000000000000000000048c8f36df1c9a4a.svg +1 -0
  256. package/seeds/4217/icons/0x20c0000000000000000000000520792dcccccccc.svg +1 -0
  257. package/seeds/4217/icons/0x20c0000000000000000000000a6da882d075a4c3.svg +4 -0
  258. package/seeds/4217/icons/0x20c000000000000000000000111111111e910f0f.svg +30 -0
  259. package/seeds/4217/icons/0x20c00000000000000000000014f22ca97301eb73.svg +1 -0
  260. package/seeds/4217/icons/0x20c0000000000000000000001621e21f71cf12fb.svg +1 -0
  261. package/seeds/4217/icons/0x20c0000000000000000000002f52d5cc21a3207b.svg +1 -0
  262. package/seeds/4217/icons/0x20c0000000000000000000003158081efd85bfc2.svg +11 -0
  263. package/seeds/4217/icons/0x20c0000000000000000000003554d28269e0f3c2.svg +1 -0
  264. package/seeds/4217/icons/0x20c000000000000000000000383a23bacb546ab9.svg +1 -0
  265. package/seeds/4217/icons/0x20c00000000000000000000042109aef2f8b28e1.png +0 -0
  266. package/seeds/4217/icons/0x20c0000000000000000000005c0bac7cef389a11.svg +1 -0
  267. package/seeds/4217/icons/0x20c0000000000000000000006fd9a167923ba194.png +0 -0
  268. package/seeds/4217/icons/0x20c0000000000000000000007f7ba549dd0251b9.svg +1 -0
  269. package/seeds/4217/icons/0x20c0000000000000000000008191667423f70e67.svg +5 -0
  270. package/seeds/4217/icons/0x20c0000000000000000000008ee4fcff88888888.svg +1 -0
  271. package/seeds/4217/icons/0x20c0000000000000000000009a4a4b17e0dc6651.svg +1 -0
  272. package/seeds/4217/icons/0x20c000000000000000000000ab02d39df30bd17e.svg +1 -0
  273. package/seeds/4217/icons/0x20c000000000000000000000ae247a1130450f09.svg +1 -0
  274. package/seeds/4217/icons/0x20c000000000000000000000aeed2ec36a54d0e5.svg +1 -0
  275. package/seeds/4217/icons/0x20c000000000000000000000b9537d11c60e8b50.svg +1 -0
  276. package/seeds/4217/icons/0x20c000000000000000000000bd95bfb69fbe6ce3.svg +1 -0
  277. package/seeds/4217/icons/0x20c000000000000000000000c412ec89d0c08be5.svg +1 -0
  278. package/seeds/4217/icons/0x20c000000000000000000000f047dd7018e50367.png +0 -0
  279. package/seeds/4217/verified-tokens.json +177 -0
  280. package/seeds/42431/icons/0x20c0000000000000000000000000000000000000.svg +25 -0
  281. package/seeds/42431/icons/0x20c0000000000000000000000000000000000001.svg +1 -0
  282. package/seeds/42431/icons/0x20c0000000000000000000000000000000000002.svg +1 -0
  283. package/seeds/42431/icons/0x20c0000000000000000000000000000000000003.svg +1 -0
  284. package/seeds/42431/icons/0x20c0000000000000000000009e8d7eb59b783726.svg +1 -0
  285. package/seeds/42431/icons/0x20c000000000000000000000d72572838bbee59c.svg +1 -0
  286. package/seeds/42431/verified-tokens.json +44 -0
  287. package/seeds/openrpc/README.md +27 -0
  288. package/seeds/openrpc/execution-apis.json +8989 -0
  289. package/src/ApiKey.test.ts +91 -0
  290. package/src/ApiKey.ts +122 -0
  291. package/src/ApiKeySource.test-d.ts +11 -0
  292. package/src/ApiKeySource.test.ts +295 -0
  293. package/src/ApiKeySource.ts +303 -0
  294. package/src/App.test-d.ts +38 -0
  295. package/src/App.test.ts +65927 -0
  296. package/src/App.ts +855 -0
  297. package/src/Assets.ts +58 -0
  298. package/src/Auth.ts +1 -0
  299. package/src/Cli.test.ts +81 -0
  300. package/src/Cli.ts +38 -0
  301. package/src/Client.test-d.ts +1438 -0
  302. package/src/Client.test.ts +134 -0
  303. package/src/Client.ts +45 -0
  304. package/src/Metrics.ts +154 -0
  305. package/src/Scope.ts +50 -0
  306. package/src/Webhooks.test.ts +49 -0
  307. package/src/Webhooks.ts +172 -0
  308. package/src/admin/Access.test.ts +130 -0
  309. package/src/admin/Access.ts +87 -0
  310. package/src/admin/App.test.ts +225 -0
  311. package/src/admin/App.ts +216 -0
  312. package/src/admin/apps/api-keys.test.ts +156 -0
  313. package/src/admin/apps/api-keys.ts +157 -0
  314. package/src/admin/apps/scopes.test.ts +52 -0
  315. package/src/admin/apps/scopes.ts +47 -0
  316. package/src/admin/apps/verified-tokens.test.ts +415 -0
  317. package/src/admin/apps/verified-tokens.ts +566 -0
  318. package/src/admin/index.ts +2 -0
  319. package/src/admin/ui/plugin.ts +135 -0
  320. package/src/admin/ui/src/components/Checkbox.tsx +22 -0
  321. package/src/admin/ui/src/components/Dialog.tsx +32 -0
  322. package/src/admin/ui/src/components/MobileProfile.tsx +31 -0
  323. package/src/admin/ui/src/components/Page.tsx +53 -0
  324. package/src/admin/ui/src/components/TempoLogo.tsx +37 -0
  325. package/src/admin/ui/src/fonts/Pilat-Demi.woff2 +0 -0
  326. package/src/admin/ui/src/fonts/Pilat-Regular.woff2 +0 -0
  327. package/src/admin/ui/src/lib/api-keys.ts +63 -0
  328. package/src/admin/ui/src/lib/client.ts +15 -0
  329. package/src/admin/ui/src/lib/config.ts +57 -0
  330. package/src/admin/ui/src/lib/query-client.ts +26 -0
  331. package/src/admin/ui/src/lib/scopes.ts +25 -0
  332. package/src/admin/ui/src/lib/verified-tokens.ts +89 -0
  333. package/src/admin/ui/src/lib/wagmi.ts +18 -0
  334. package/src/admin/ui/src/routeTree.gen.ts +104 -0
  335. package/src/admin/ui/src/router.tsx +25 -0
  336. package/src/admin/ui/src/routes/__root.tsx +219 -0
  337. package/src/admin/ui/src/routes/api-keys.tsx +329 -0
  338. package/src/admin/ui/src/routes/index.tsx +7 -0
  339. package/src/admin/ui/src/routes/verified-tokens.tsx +331 -0
  340. package/src/admin/ui/src/styles.css +20 -0
  341. package/src/admin/ui/tsconfig.json +16 -0
  342. package/src/admin/ui/vite.config.ts +29 -0
  343. package/src/admin/ui.generated.ts +60 -0
  344. package/src/apps/activities.test.ts +1020 -0
  345. package/src/apps/activities.ts +3934 -0
  346. package/src/apps/addresses.ts +24 -0
  347. package/src/apps/balances.test.ts +274 -0
  348. package/src/apps/balances.ts +533 -0
  349. package/src/apps/blocks.test.ts +385 -0
  350. package/src/apps/blocks.ts +896 -0
  351. package/src/apps/coingecko.test.ts +218 -0
  352. package/src/apps/coingecko.ts +1383 -0
  353. package/src/apps/exchanges.test.ts +958 -0
  354. package/src/apps/exchanges.ts +4773 -0
  355. package/src/apps/fee-amm.test.ts +321 -0
  356. package/src/apps/fee-amm.ts +1121 -0
  357. package/src/apps/indexer.test.ts +153 -0
  358. package/src/apps/indexer.ts +330 -0
  359. package/src/apps/receipts.test.ts +285 -0
  360. package/src/apps/receipts.ts +1193 -0
  361. package/src/apps/rpc.test.ts +302 -0
  362. package/src/apps/rpc.ts +222 -0
  363. package/src/apps/tokenlist.test.ts +80 -0
  364. package/src/apps/tokenlist.ts +207 -0
  365. package/src/apps/tokens.test.ts +1062 -0
  366. package/src/apps/tokens.ts +2292 -0
  367. package/src/apps/transactions.test.ts +408 -0
  368. package/src/apps/transactions.ts +1363 -0
  369. package/src/apps/transfers.test.ts +371 -0
  370. package/src/apps/transfers.ts +1116 -0
  371. package/src/apps/verified-tokens.test.ts +114 -0
  372. package/src/apps/verified-tokens.ts +295 -0
  373. package/src/apps/webhooks.test.ts +1937 -0
  374. package/src/apps/webhooks.ts +2926 -0
  375. package/src/cloudflare.ts +55 -0
  376. package/src/index.ts +15 -0
  377. package/src/internal/Auth.test.ts +980 -0
  378. package/src/internal/Auth.ts +1012 -0
  379. package/src/internal/Cache.test.ts +240 -0
  380. package/src/internal/Cache.ts +262 -0
  381. package/src/internal/Cursor.test.ts +100 -0
  382. package/src/internal/Cursor.ts +149 -0
  383. package/src/internal/EdgeCache.test.ts +234 -0
  384. package/src/internal/EdgeCache.ts +224 -0
  385. package/src/internal/Errors.ts +14 -0
  386. package/src/internal/Log.test.ts +244 -0
  387. package/src/internal/Log.ts +216 -0
  388. package/src/internal/MetricSink.ts +220 -0
  389. package/src/internal/Mpp.test.ts +64 -0
  390. package/src/internal/Mpp.ts +141 -0
  391. package/src/internal/OpenApi.fonts.ts +38 -0
  392. package/src/internal/OpenApi.logo.ts +18 -0
  393. package/src/internal/OpenApi.ts +488 -0
  394. package/src/internal/Path.test.ts +14 -0
  395. package/src/internal/Path.ts +15 -0
  396. package/src/internal/RateLimit.test.ts +93 -0
  397. package/src/internal/RateLimit.ts +93 -0
  398. package/src/internal/Response.ts +131 -0
  399. package/src/internal/Schema.test.ts +34 -0
  400. package/src/internal/Schema.ts +523 -0
  401. package/src/internal/Store.test.ts +545 -0
  402. package/src/internal/Store.ts +526 -0
  403. package/src/internal/Tidx.local.test.ts +33 -0
  404. package/src/internal/Tidx.test.ts +294 -0
  405. package/src/internal/Tidx.ts +238 -0
  406. package/src/internal/Timing.test.ts +22 -0
  407. package/src/internal/Timing.ts +62 -0
  408. package/src/internal/Ttl.test.ts +18 -0
  409. package/src/internal/Ttl.ts +59 -0
  410. package/src/internal/Value.ts +114 -0
  411. package/src/internal/VerifiedTokens.test.ts +333 -0
  412. package/src/internal/VerifiedTokens.ts +645 -0
  413. package/src/internal/Viem.test.ts +115 -0
  414. package/src/internal/Viem.ts +165 -0
  415. package/src/internal/WebhookDestination.test.ts +648 -0
  416. package/src/internal/WebhookDestination.ts +331 -0
  417. package/src/internal/Webhooks.test.ts +668 -0
  418. package/src/internal/Webhooks.ts +940 -0
  419. package/src/internal/webhookDestinations/betterStack.ts +91 -0
  420. package/src/internal/webhookDestinations/slack.ts +496 -0
  421. package/src/internal/webhookDestinations/url.ts +128 -0
  422. package/src/openrpc.json +5302 -0
@@ -0,0 +1,1116 @@
1
+ import { Hono, type Context } from 'hono'
2
+ import { Address, Hex } from 'ox'
3
+ import { Addresses } from 'viem/tempo'
4
+ import * as z from 'zod/mini'
5
+
6
+ import type * as App from '../App.js'
7
+ import * as Auth from '../internal/Auth.js'
8
+ import * as Cache from '../internal/Cache.js'
9
+ import * as Cursor from '../internal/Cursor.js'
10
+ import * as Mpp from '../internal/Mpp.js'
11
+ import * as OpenApi from '../internal/OpenApi.js'
12
+ import * as Response from '../internal/Response.js'
13
+ import * as Schema from '../internal/Schema.js'
14
+ import * as Store from '../internal/Store.js'
15
+ import type * as Tidx from '../internal/Tidx.js'
16
+ import * as Timing from '../internal/Timing.js'
17
+ import * as Ttl from '../internal/Ttl.js'
18
+ import * as Value from '../internal/Value.js'
19
+ import * as VerifiedTokens from '../internal/VerifiedTokens.js'
20
+ import * as Tokens from './tokens.js'
21
+
22
+ // TIP-20 tokens emit the standard ERC-20 Transfer event. Supplying the
23
+ // signature exposes a decoded `Transfer` CTE (with `from`/`to`/`value`) to
24
+ // the indexer SQL.
25
+ export const signature = 'event Transfer(address indexed from, address indexed to, uint256 value)'
26
+
27
+ // A memo'd TIP-20 transfer emits `TransferWithMemo` alongside the plain
28
+ // `Transfer`; the two rows match on `(tx_hash, from, to, amount, token)`. A
29
+ // text memo is decoded into `memo`; an MPP attribution fingerprint memo is
30
+ // resolved into `attribution` instead.
31
+ const memoSignature =
32
+ 'event TransferWithMemo(address indexed from, address indexed to, uint256 amount, bytes32 indexed memo)'
33
+
34
+ /** Zod schemas owned by the transfer handlers. */
35
+ 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
+ */
51
+ export const SourceToken = z
52
+ .object({
53
+ 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
+ currency: z
64
+ .optional(z.string())
65
+ .check(
66
+ z.describe(
67
+ 'The currency label for this token, such as `USD` for USD-denominated stablecoins.',
68
+ ),
69
+ z.meta({ examples: ['USD'] }),
70
+ ),
71
+ decimals: z
72
+ .optional(z.number().check(z.int(), z.nonnegative()))
73
+ .check(
74
+ z.describe(
75
+ 'The number of decimal places the token uses; Tempo stablecoins typically use 6.',
76
+ ),
77
+ z.meta({ examples: [6] }),
78
+ ),
79
+ logoUri: z.optional(z.string()).check(
80
+ z.describe('A URL for the token’s logo image, when one is available.'),
81
+ z.meta({
82
+ examples: [
83
+ 'https://api.tempo.xyz/data/4217/icons/0x20c0000000000000000000008f5425160ebe5525.svg',
84
+ ],
85
+ }),
86
+ ),
87
+ name: z
88
+ .optional(z.string())
89
+ .check(z.describe('The token’s human-readable name.'), z.meta({ examples: ['USD Coin'] })),
90
+ symbol: z
91
+ .optional(z.string())
92
+ .check(
93
+ z.describe('The short ticker symbol wallets and apps show for this token.'),
94
+ z.meta({ examples: ['USDC'] }),
95
+ ),
96
+ verified: z
97
+ .optional(z.boolean())
98
+ .check(
99
+ z.describe('Whether this token is in Tempo’s curated verified token list.'),
100
+ z.meta({ examples: [true] }),
101
+ ),
102
+ })
103
+ .check(
104
+ z.describe(
105
+ 'The token and amount that moved in this transfer, with token metadata added when you request `include=token`.',
106
+ ),
107
+ )
108
+
109
+ /**
110
+ * Filter predicates shared verbatim by the `GET /transfers` list query
111
+ * ({@link getTransfers.Query}, which spreads `Predicates.shape`) and `transfer`
112
+ * webhook subscriptions (`Webhooks.schema.TransferFilters`), so the two never
113
+ * drift. Read-only concerns (pagination, windowing, ordering, `include`,
114
+ * `chainId`) are *not* here — they live only on the list query and have no
115
+ * meaning for an open-ended subscription. Consumers that need different
116
+ * per-field docs (e.g. the list query's mutually-exclusive `address`) override
117
+ * that one field.
118
+ */
119
+ export const Predicates = z.object({
120
+ address: z
121
+ .optional(Schema.Address)
122
+ .check(
123
+ z.describe('Match transfers where this account is either the sender or the recipient.'),
124
+ ),
125
+ recipient: z
126
+ .optional(Schema.Address)
127
+ .check(z.describe('Match transfers sent to this recipient address (`to`).')),
128
+ sender: z
129
+ .optional(Schema.Address)
130
+ .check(z.describe('Match transfers sent from this sender address (`from`).')),
131
+ token: z
132
+ .optional(Schema.TokenAddress)
133
+ .check(z.describe('Match transfers for this TIP-20 token contract address.')),
134
+ })
135
+
136
+ /** Schemas for the getTransfers (list) operation. */
137
+ export namespace getTransfers {
138
+ /** Optional related resources that callers opt into via `include`. */
139
+ export const Include = z
140
+ .enum(['crossToken', 'memo', 'token', 'totalCount'])
141
+ .check(z.describe('Related resources to include only when you request them with `include`.'))
142
+
143
+ /**
144
+ * 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.
148
+ */
149
+ export const includeQuery = Schema.includeQuery(
150
+ Include,
151
+ 'Comma-separated related resources to include, such as `token,memo,crossToken,totalCount`.',
152
+ )
153
+
154
+ /**
155
+ * Query parameters for top-level transfer list requests. `address` is a
156
+ * convenience for "either side" — passing it alongside an explicit
157
+ * `sender` or `recipient` is rejected as ambiguous (the helper would have
158
+ * to AND/OR them in a non-obvious way).
159
+ */
160
+ export const Query = z
161
+ .strictObject({
162
+ // Predicate fields (`address`, `recipient`, `sender`, `token`) are shared
163
+ // verbatim with `transfer` webhook filters via `schema.Predicates`;
164
+ // `address` is overridden below for its list-only mutual-exclusion note.
165
+ ...schema.Predicates.shape,
166
+ address: z
167
+ .optional(Schema.Address)
168
+ .check(
169
+ z.describe(
170
+ 'Only include transfers where this account is the sender or recipient. Use this shortcut by itself, not with `sender` or `recipient`.',
171
+ ),
172
+ ),
173
+ 'blockNumber.from': Schema.blockNumberBound('transfers', 'from'),
174
+ 'blockNumber.to': Schema.blockNumberBound('transfers', 'to'),
175
+ chainId: Schema.ChainIdQuery,
176
+ cursor: Schema.Cursor,
177
+ include: includeQuery,
178
+ limit: Schema.Limit,
179
+ order: Schema.Order,
180
+ page: Schema.Page,
181
+ 'timestamp.from': Schema.timestampBound('transfers', 'from'),
182
+ 'timestamp.to': Schema.timestampBound('transfers', 'to'),
183
+ })
184
+ .check(
185
+ z.refine((query) => !(query.address && (query.sender || query.recipient)), {
186
+ error:
187
+ '`address` is a shortcut for `sender` or `recipient`; pass either `address` or one explicit side, not both.',
188
+ path: ['address'],
189
+ }),
190
+ ...Schema.pageChecks(),
191
+ )
192
+ .check(z.describe('Query parameters for listing token transfer events.'))
193
+
194
+ /**
195
+ * A single transfer row. The token contract is carried per-row via
196
+ * `sourceToken.address` because `/transfers` is unscoped and rows can span
197
+ * multiple contracts.
198
+ */
199
+ export const Transfer = z
200
+ .object({
201
+ attribution: z
202
+ .optional(z.string())
203
+ .check(
204
+ z.describe(
205
+ 'The resolved MPP service name, when the transfer memo matches a known service fingerprint. Returned when you request `include=memo`.',
206
+ ),
207
+ z.meta({ examples: ['example-service'] }),
208
+ ),
209
+ blockNumber: z
210
+ .number()
211
+ .check(
212
+ z.int(),
213
+ z.nonnegative(),
214
+ z.describe('The block number where this transfer was recorded.'),
215
+ z.meta({ examples: [23456789] }),
216
+ ),
217
+ destinationToken: z
218
+ .optional(schema.SourceToken)
219
+ .check(
220
+ 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`.',
222
+ ),
223
+ ),
224
+ id: z.string().check(
225
+ z.describe(
226
+ 'A stable resource ID for this transfer, built from `${transactionHash}-${logIndex}`.',
227
+ ),
228
+ z.meta({
229
+ examples: ['0x515801d7f9a5ac705e793e85904c9c69b3f1694b465cc8fb6ba3f0298dc82665-0'],
230
+ }),
231
+ ),
232
+ logIndex: z
233
+ .number()
234
+ .check(
235
+ z.int(),
236
+ z.nonnegative(),
237
+ z.describe('The transfer event’s log index within the block.'),
238
+ z.meta({ examples: [0] }),
239
+ ),
240
+ memo: z
241
+ .optional(z.string())
242
+ .check(
243
+ z.describe(
244
+ 'The decoded text memo from the matching `TransferWithMemo` event, when one is present. Returned when you request `include=memo`.',
245
+ ),
246
+ z.meta({ examples: ['Payment for invoice 123'] }),
247
+ ),
248
+ recipient: Schema.Address.check(
249
+ z.describe('The account address that received the tokens.'),
250
+ ),
251
+ 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.')),
253
+ timestamp: z.iso
254
+ .datetime()
255
+ .check(
256
+ z.describe('The block timestamp when this transfer was recorded.'),
257
+ z.meta({ examples: ['2024-01-01T00:00:00Z'] }),
258
+ ),
259
+ transactionHash: Schema.Hash.check(
260
+ z.describe('The hash of the transaction that emitted this transfer event.'),
261
+ ),
262
+ })
263
+ .check(
264
+ z.describe(
265
+ 'One TIP-20 token transfer event, optionally including a payment memo for reconciliation or invoices.',
266
+ ),
267
+ )
268
+
269
+ /** Page of TIP-20 token transfers across the chain, ordered by block then log index. */
270
+ export const Response = z
271
+ .object({
272
+ data: z.array(Transfer).check(z.describe('The transfers in this page.')),
273
+ // 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.
276
+ meta: z
277
+ .optional(Schema.CountMeta)
278
+ .check(z.describe('Page-level resources you requested with `include`.')),
279
+ nextCursor: Schema.NextCursor,
280
+ })
281
+ .check(z.describe('A page of TIP-20 token transfer events across the chain.'))
282
+ }
283
+ }
284
+
285
+ /** Creates transfer handlers. */
286
+ export function transfers() {
287
+ return new Hono<App.Environment>().get(
288
+ '/',
289
+ Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }),
290
+ OpenApi.validate('query', schema.getTransfers.Query, {
291
+ code: 'query_invalid',
292
+ message: 'Invalid query parameters',
293
+ }),
294
+ OpenApi.describeRoute({
295
+ description: 'List token transfer events across Tempo.',
296
+ operationId: 'getTransfers',
297
+ responses: OpenApi.responses({
298
+ errors: { 502: 'The upstream indexer could not serve this request.' },
299
+ success: {
300
+ description: 'A page of token transfer events.',
301
+ schema: schema.getTransfers.Response,
302
+ },
303
+ }),
304
+ summary: 'List transfers',
305
+ tags: ['Transfers'],
306
+ }),
307
+ Cache.response<App.Environment>({
308
+ cacheControl: Cache.policies.feed,
309
+ name: 'tempo-api:transfers:v1',
310
+ key: (c) => Cache.urlKey(c, schema.getTransfers.Query),
311
+ }),
312
+ async (c) => {
313
+ if (Auth.narrowAccess) return Auth.paidAccessError(c)
314
+ if (OpenApi.narrowValidation)
315
+ return OpenApi.validationError(c, {
316
+ code: 'query_invalid',
317
+ message: 'Invalid query parameters',
318
+ })
319
+
320
+ const options = c.req.valid('query')
321
+ const chainId = options.chainId ?? c.get('chainId')
322
+
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)
327
+
328
+ // Filters shared by the page query and the opt-in total count, so the
329
+ // count always matches the page it annotates.
330
+ const predicates = {
331
+ chainId,
332
+ eitherSide: options.address,
333
+ fromBlock: options['blockNumber.from'],
334
+ fromTimestamp: options['timestamp.from'],
335
+ recipient: options.recipient,
336
+ sender: options.sender,
337
+ toBlock: options['blockNumber.to'],
338
+ toTimestamp: options['timestamp.to'],
339
+ token: options.token,
340
+ }
341
+
342
+ try {
343
+ // `totalCount` is opt-in and independent of the page rows, so kick it
344
+ // off concurrently with the page query. It is best-effort: a count
345
+ // failure (e.g. upstream timeout) omits `meta` rather than failing the
346
+ // whole page.
347
+ const countPromise = options.include.includes('totalCount')
348
+ ? count(c, {
349
+ ...predicates,
350
+ crossToken: options.include.includes('crossToken'),
351
+ timing: 'transfers_count',
352
+ }).catch(() => undefined)
353
+ : undefined
354
+
355
+ const page = await query(c, {
356
+ ...predicates,
357
+ cursor: options.cursor,
358
+ limit: options.limit,
359
+ order: options.order,
360
+ page: options.page,
361
+ timing: 'transfers',
362
+ })
363
+
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) => ({
370
+ blockNumber: row.blockNumber,
371
+ id: `${row.transactionHash}-${row.logIndex}`,
372
+ logIndex: row.logIndex,
373
+ recipient: row.recipient,
374
+ sender: row.sender,
375
+ sourceToken: { address: row.address, amount: row.amount },
376
+ timestamp: row.timestamp,
377
+ transactionHash: row.transactionHash,
378
+ }))
379
+
380
+ // Memo/attribution are opt-in: one `TransferWithMemo` lookup bounded
381
+ // to the page's transactions resolves both — the decoded text `memo`,
382
+ // and `attribution` when the memo is a known MPP service fingerprint.
383
+ if (options.include.includes('memo') && data.length > 0) {
384
+ const memoByRow = await memoMap(c, { chainId, rows: data })
385
+ data = data.map((transfer) => {
386
+ const entry = memoByRow.get(
387
+ transferKey(
388
+ transfer.transactionHash,
389
+ transfer.sender,
390
+ transfer.recipient,
391
+ transfer.sourceToken.amount,
392
+ transfer.sourceToken.address,
393
+ ),
394
+ )
395
+ if (!entry) return transfer
396
+ return {
397
+ ...transfer,
398
+ ...(entry.attribution ? { attribution: entry.attribution } : {}),
399
+ ...(entry.memo ? { memo: entry.memo } : {}),
400
+ }
401
+ })
402
+ }
403
+ // Cross-token routing is opt-in. One `Transfer` lookup bounded to the
404
+ // page's transactions folds the account server's approve+swap+forward
405
+ // multicall into a single row on the delivery leg: `sourceToken` becomes
406
+ // the token swapped into the DEX and `destinationToken` the token
407
+ // delivered to the recipient. The intermediate DEX legs and the fee leg
408
+ // are dropped from the page. Folding anchors on the delivery leg (whose
409
+ // own event token is the delivered token), so a `token=` filter matches
410
+ // cross-token rows by their destination token, and each transfer still
411
+ // appears exactly once across pages.
412
+ if (options.include.includes('crossToken') && data.length > 0) {
413
+ const { fold, suppress } = await crossTokenMap(c, { chainId, rows: data })
414
+ data = data.flatMap((transfer) => {
415
+ const key = eventKey(transfer.transactionHash, transfer.logIndex)
416
+ const folded = fold.get(key)
417
+ if (folded)
418
+ return [
419
+ {
420
+ ...transfer,
421
+ destinationToken: folded.destinationToken,
422
+ recipient: folded.recipient,
423
+ sender: folded.sender,
424
+ sourceToken: folded.sourceToken,
425
+ },
426
+ ]
427
+ return suppress.has(key) ? [] : [transfer]
428
+ })
429
+ }
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', () =>
441
+ 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
+ ),
451
+ ),
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
459
+ }
460
+ data = data.map((transfer) => ({
461
+ ...transfer,
462
+ sourceToken: withToken(transfer.sourceToken),
463
+ ...(transfer.destinationToken
464
+ ? { destinationToken: withToken(transfer.destinationToken) }
465
+ : {}),
466
+ }))
467
+ }
468
+
469
+ const meta = countPromise ? await countPromise : undefined
470
+
471
+ return c.json(
472
+ Response.validated(schema.getTransfers.Response, {
473
+ data,
474
+ ...(meta ? { meta } : {}),
475
+ nextCursor: page.nextCursor,
476
+ }),
477
+ 200,
478
+ )
479
+ } catch (cause) {
480
+ return Response.upstream(c, cause)
481
+ }
482
+ },
483
+ )
484
+ }
485
+
486
+ /**
487
+ * Shared TIP-20 `Transfer` page query. Used by `GET /transfers` and the
488
+ * webhook poller. Builds the signature-decoded `Transfer` CTE query, applies
489
+ * opaque keyset pagination on `(block_num, log_idx)`, and decodes rows into a
490
+ * strongly-typed intermediate shape that each caller maps to its own public
491
+ * response.
492
+ *
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.
497
+ *
498
+ * Memoization lives inside the helper under one canonical `transfers:v1:…`
499
+ * key derived from every filter — so equivalent filter combinations share a
500
+ * single cache entry. Callers supply only a `timing` label for Server-Timing.
501
+ */
502
+ export function query(c: Context<App.Environment>, options: query.Options) {
503
+ const { chainId, timing, ...rest } = options
504
+ const store = c.get('store')
505
+ const tidx = c.get('getTidx')(chainId)
506
+ const cursor = options.cursor ? Cursor.decode(options.cursor, ['int', 'int']) : undefined
507
+
508
+ return Timing.time(c, timing, () =>
509
+ Store.memoize(
510
+ async () => {
511
+ const page = await scan({ tidx }, { chainId, ...rest })
512
+ return {
513
+ data: page.items.map((item) => item.data),
514
+ nextCursor: page.nextCursor,
515
+ }
516
+ },
517
+ {
518
+ key: `transfers:v1:${chainId}:${options.order}:${options.token ?? ''}:${options.eitherSide ?? ''}:${options.sender ?? ''}:${options.recipient ?? ''}:${options.fromBlock ?? ''}:${options.toBlock ?? ''}:${options.fromTimestamp ?? ''}:${options.toTimestamp ?? ''}:${cursor ? `cursor:${cursor[0]}:${cursor[1]}` : options.page !== undefined && options.page > 1 ? `page:${options.page}` : 'head'}:${options.limit}`,
519
+ store,
520
+ ttl: Ttl.seconds(15),
521
+ },
522
+ ),
523
+ )
524
+ }
525
+
526
+ export declare namespace query {
527
+ /** Options for the shared TIP-20 `Transfer` page query. */
528
+ type Options = {
529
+ chainId: z.output<typeof Schema.ChainId>
530
+ /** Opaque keyset cursor anchoring the page; omit for the head page. */
531
+ cursor?: string | undefined
532
+ /** Restrict to rows where this address appears on either side. */
533
+ eitherSide?: z.output<typeof Schema.Address> | undefined
534
+ fromBlock?: number | undefined
535
+ fromTimestamp?: string | undefined
536
+ limit: number
537
+ /** 1-indexed page number (positional pagination; exclusive with `cursor`). */
538
+ page?: number | undefined
539
+ order: 'asc' | 'desc'
540
+ recipient?: z.output<typeof Schema.Address> | undefined
541
+ sender?: z.output<typeof Schema.Address> | undefined
542
+ /** Server-Timing label for the caller's surface. */
543
+ timing: string
544
+ toBlock?: number | undefined
545
+ toTimestamp?: string | undefined
546
+ /** Restrict to a single TIP-20 token contract. */
547
+ token?: z.output<typeof Schema.Address> | undefined
548
+ }
549
+ /** A decoded `Transfer` row before per-surface shaping. */
550
+ type Row = {
551
+ /** Token contract that emitted the event. */
552
+ address: z.output<typeof Schema.Address>
553
+ amount: string
554
+ blockNumber: number
555
+ logIndex: number
556
+ recipient: z.output<typeof Schema.Address>
557
+ sender: z.output<typeof Schema.Address>
558
+ timestamp: string
559
+ transactionHash: z.output<typeof Schema.Hash>
560
+ }
561
+ type Page = {
562
+ data: readonly Row[]
563
+ nextCursor: string | null
564
+ }
565
+ }
566
+
567
+ /**
568
+ * Context-free core of {@link query}: builds the signature-decoded `Transfer`
569
+ * CTE query, applies keyset pagination on `(block_num, log_idx)`, and decodes
570
+ * rows. Used by the Hono routes (via {@link query}, which adds Server-Timing +
571
+ * memoization) and by the webhook poller (uncached, ascending, per-row cursors).
572
+ */
573
+ export async function scan(deps: scan.Deps, options: scan.Options): Promise<scan.Page> {
574
+ const { chainId, limit, order } = options
575
+ const { tidx } = deps
576
+ const direction = order === 'asc' ? 'ASC' : 'DESC'
577
+ // Keyset pagination: anchor the page below the previous row's `(block, log_idx)`
578
+ // position instead of a numeric offset, so rows arriving at the head can't
579
+ // shift items across pages. A malformed cursor falls back to the head page.
580
+ const cursor = options.cursor ? Cursor.decode(options.cursor, ['int', 'int']) : undefined
581
+ // Bounded positional lane (exclusive with `cursor` at the schema): page 1 is
582
+ // the head page, so it shares the head's SQL and cache entries; deeper pages
583
+ // translate to a SQL OFFSET of `(page - 1) * limit` rows.
584
+ const offset =
585
+ options.page !== undefined && options.page > 1 ? (options.page - 1) * limit : undefined
586
+
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.
591
+ const filters = filterClauses(options)
592
+ if (cursor !== undefined)
593
+ filters.push(
594
+ Cursor.keyset([
595
+ { literal: Cursor.literal(cursor[0]!, 'int'), name: 'block_num', order },
596
+ { literal: Cursor.literal(cursor[1]!, 'int'), name: 'log_idx', order },
597
+ ]),
598
+ )
599
+ const where = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''
600
+
601
+ // Fetch one extra row to detect `hasMore` without a separate count query. The
602
+ // global minimum page size (`Schema.minLimit`) keeps this at or above the
603
+ // indexer's floor for signature-decoded `Transfer` queries.
604
+ const result = await tidx.fetch({
605
+ chainId,
606
+ query: `
607
+ SELECT "from", "to", address, value, tx_hash, block_num, log_idx, block_timestamp
608
+ FROM Transfer
609
+ ${where}
610
+ ORDER BY block_num ${direction}, log_idx ${direction}
611
+ LIMIT ${limit + 1}${offset !== undefined ? ` OFFSET ${offset}` : ''}
612
+ `,
613
+ signatures: [signature],
614
+ })
615
+
616
+ // The next page anchors below the last fetched row's `(block, log_idx)`.
617
+ const page = Cursor.paginate({
618
+ rows: result.rows,
619
+ limit,
620
+ key: (row) => {
621
+ const block = Value.toNumber(row['block_num'])
622
+ const index = Value.toNumber(row['log_idx'])
623
+ return block !== undefined && index !== undefined ? [block, index] : undefined
624
+ },
625
+ })
626
+
627
+ const items: scan.Item[] = []
628
+ for (const row of page.rows) {
629
+ const tokenAddress = Schema.Address.safeParse(row['address'])
630
+ const from = Schema.Address.safeParse(row['from'])
631
+ const to = Schema.Address.safeParse(row['to'])
632
+ const transactionHash = Schema.Hash.safeParse(row['tx_hash'])
633
+ const amount = Value.toIntegerString(row['value'])
634
+ const blockNumber = Value.toNumber(row['block_num'])
635
+ const logIndex = Value.toNumber(row['log_idx'])
636
+ const timestamp = Value.toIsoDateTime(row['block_timestamp'])
637
+ if (
638
+ tokenAddress.success &&
639
+ from.success &&
640
+ to.success &&
641
+ transactionHash.success &&
642
+ amount !== undefined &&
643
+ blockNumber !== undefined &&
644
+ logIndex !== undefined &&
645
+ timestamp !== undefined
646
+ )
647
+ items.push({
648
+ cursor: [blockNumber, logIndex],
649
+ data: {
650
+ address: tokenAddress.data,
651
+ amount,
652
+ blockNumber,
653
+ logIndex,
654
+ recipient: to.data,
655
+ sender: from.data,
656
+ timestamp,
657
+ transactionHash: transactionHash.data,
658
+ },
659
+ })
660
+ }
661
+
662
+ return { hasMore: page.hasMore, items, limit, nextCursor: page.nextCursor }
663
+ }
664
+
665
+ export declare namespace scan {
666
+ /** Dependencies for {@link scan}. */
667
+ type Deps = {
668
+ /** TIDX query client for the target chain. */
669
+ tidx: Tidx.Client
670
+ }
671
+ /** Options for {@link scan} (everything {@link query.Options} has except `timing`). */
672
+ type Options = Omit<query.Options, 'timing'>
673
+ /** A decoded `Transfer` row plus its `(block_num, log_idx)` cursor. */
674
+ type Item = {
675
+ /** Keyset position of the row. */
676
+ cursor: readonly [blockNumber: number, logIndex: number]
677
+ /** The decoded row before per-surface shaping. */
678
+ data: query.Row
679
+ }
680
+ /** A page of scanned `Transfer` rows. */
681
+ type Page = {
682
+ hasMore: boolean
683
+ items: readonly Item[]
684
+ limit: number
685
+ nextCursor: string | null
686
+ }
687
+ }
688
+
689
+ /**
690
+ * Builds the shared `WHERE` filter clauses for the `Transfer` feed: contract /
691
+ * subject / sender / recipient / block / timestamp predicates. Shared verbatim
692
+ * by the page query ({@link scan}) and the capped count ({@link countScan}) so a
693
+ * `totalCount` always matches the page it annotates; the keyset cursor clause is
694
+ * page-only and added by {@link scan} on top of these.
695
+ */
696
+ function filterClauses(options: filterClauses.Options): string[] {
697
+ const { eitherSide, fromBlock, fromTimestamp, recipient, sender, toBlock, toTimestamp, token } =
698
+ options
699
+ const filters: string[] = []
700
+ if (token !== undefined) filters.push(`address = '${token}'`)
701
+ if (eitherSide !== undefined) filters.push(`("from" = '${eitherSide}' OR "to" = '${eitherSide}')`)
702
+ if (sender !== undefined) filters.push(`"from" = '${sender}'`)
703
+ if (recipient !== undefined) filters.push(`"to" = '${recipient}'`)
704
+ if (fromBlock !== undefined) filters.push(`block_num >= ${fromBlock}`)
705
+ if (toBlock !== undefined) filters.push(`block_num <= ${toBlock}`)
706
+ if (fromTimestamp !== undefined) filters.push(`block_timestamp >= '${fromTimestamp}'`)
707
+ if (toTimestamp !== undefined) filters.push(`block_timestamp <= '${toTimestamp}'`)
708
+ return filters
709
+ }
710
+
711
+ declare namespace filterClauses {
712
+ /** The non-pagination filter fields of {@link scan.Options}. */
713
+ type Options = Pick<
714
+ scan.Options,
715
+ | 'eitherSide'
716
+ | 'fromBlock'
717
+ | 'fromTimestamp'
718
+ | 'recipient'
719
+ | 'sender'
720
+ | 'toBlock'
721
+ | 'toTimestamp'
722
+ | 'token'
723
+ >
724
+ }
725
+
726
+ /**
727
+ * Capped total-row count for the `Transfer` feed, sharing {@link query}'s
728
+ * filters (minus pagination) so the count matches the page it annotates. Timed
729
+ * and memoized like {@link query}; the result feeds `meta.totalCount`/`totalCountCapped`
730
+ * when a caller opts in via `include=totalCount`.
731
+ */
732
+ export function count(c: Context<App.Environment>, options: count.Options): Promise<count.Result> {
733
+ const { chainId, timing, ...rest } = options
734
+ const store = c.get('store')
735
+ const tidx = c.get('getTidx')(chainId)
736
+
737
+ return Timing.time(c, timing, () =>
738
+ Store.memoize(() => countScan({ tidx }, { chainId, ...rest }), {
739
+ key: `transfers:count:v1:${chainId}:${rest.crossToken ? 'fold' : 'raw'}:${rest.token ?? ''}:${rest.eitherSide ?? ''}:${rest.sender ?? ''}:${rest.recipient ?? ''}:${rest.fromBlock ?? ''}:${rest.toBlock ?? ''}:${rest.fromTimestamp ?? ''}:${rest.toTimestamp ?? ''}`,
740
+ store,
741
+ ttl: Ttl.seconds(15),
742
+ }),
743
+ )
744
+ }
745
+
746
+ export declare namespace count {
747
+ /** Options for the capped `Transfer` count: {@link query}'s filters plus `timing`. */
748
+ type Options = Pick<
749
+ query.Options,
750
+ | 'chainId'
751
+ | 'eitherSide'
752
+ | 'fromBlock'
753
+ | 'fromTimestamp'
754
+ | 'recipient'
755
+ | 'sender'
756
+ | 'timing'
757
+ | 'toBlock'
758
+ | 'toTimestamp'
759
+ | 'token'
760
+ > & {
761
+ /** Count folded cross-token transfers instead of raw `Transfer` logs. */
762
+ crossToken?: boolean | undefined
763
+ }
764
+ /** A capped count result: `totalCountCapped` is `true` when the count hit `Schema.countCap`. */
765
+ type Result = { totalCountCapped: boolean; totalCount: number }
766
+ }
767
+
768
+ /**
769
+ * Context-free core of {@link count}: runs a capped `count(*)` over the
770
+ * signature-decoded `Transfer` CTE under the shared {@link filterClauses}. The
771
+ * inner `LIMIT Schema.countCap` bounds the scan so the aggregate can't exceed
772
+ * the indexer's execution budget on the 10⁹-plus row table (an unbounded count
773
+ * there blows the 5s `max_execution_time`); a result equal to the cap is a
774
+ * lower bound, surfaced as `totalCountCapped: true`.
775
+ */
776
+ export async function countScan(
777
+ deps: scan.Deps,
778
+ options: countScan.Options,
779
+ ): Promise<count.Result> {
780
+ const { chainId } = options
781
+ const { tidx } = deps
782
+ const filters = filterClauses(options)
783
+ const where = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''
784
+
785
+ // Folded count: cross-token transfers collapse several `Transfer` legs into
786
+ // one row, so a raw `count(*)` would overcount. Scan the capped `Transfer`
787
+ // rows, classify each transaction, and subtract the folded-away legs. Bounded
788
+ // by `countCap` like the raw count, so it stays within the indexer's budget.
789
+ if (options.crossToken) {
790
+ const result = await tidx.fetch({
791
+ chainId,
792
+ query: `
793
+ SELECT tx_hash, log_idx, "from", "to", address, value
794
+ FROM Transfer
795
+ ${where}
796
+ ORDER BY block_num, log_idx
797
+ LIMIT ${Schema.countCap}
798
+ `,
799
+ signatures: [signature],
800
+ })
801
+ const rawCount = result.rows.length
802
+ let suppressed = 0
803
+ for (const legs of legsByTransaction(result.rows).values()) {
804
+ const bundle = classifyCrossToken(legs)
805
+ if (bundle) suppressed += bundle.suppressLogIndexes.length
806
+ }
807
+ return { totalCountCapped: rawCount >= Schema.countCap, totalCount: rawCount - suppressed }
808
+ }
809
+
810
+ const result = await tidx.fetch({
811
+ chainId,
812
+ query: `
813
+ SELECT count(*) AS total
814
+ FROM (
815
+ SELECT 1
816
+ FROM Transfer
817
+ ${where}
818
+ LIMIT ${Schema.countCap}
819
+ ) AS capped
820
+ `,
821
+ signatures: [signature],
822
+ })
823
+
824
+ const totalCount = Value.toNumber(result.rows[0]?.['total']) ?? 0
825
+ return { totalCountCapped: totalCount >= Schema.countCap, totalCount }
826
+ }
827
+
828
+ export declare namespace countScan {
829
+ /** Options for {@link countScan}: the shared filters plus the target chain. */
830
+ type Options = filterClauses.Options &
831
+ Pick<scan.Options, 'chainId'> & {
832
+ /** Count folded cross-token transfers instead of raw `Transfer` logs. */
833
+ crossToken?: boolean | undefined
834
+ }
835
+ }
836
+
837
+ /** `'tx1', 'tx2', …` SQL `IN` list of the page's distinct transaction hashes. */
838
+ function txInList(rows: memoMap.Options['rows']): string {
839
+ return [...new Set(rows.map((row) => row.transactionHash.toLowerCase()))]
840
+ .map((hash) => `'${hash}'`)
841
+ .join(', ')
842
+ }
843
+
844
+ /**
845
+ * `block_num BETWEEN min AND max` bound covering the page's rows. Event-CTE
846
+ * predicates on `tx_hash` cannot prune `logs` (sorted by block position), so
847
+ * a bare `tx_hash IN (…)` scans to genesis; pairing it with the page's block
848
+ * range keeps the lookup a bounded range read. Callers only invoke the memo
849
+ * lookup for non-empty pages.
850
+ */
851
+ function blockBound(rows: memoMap.Options['rows']): string {
852
+ const blocks = rows.map((row) => row.blockNumber)
853
+ return `block_num BETWEEN ${Math.min(...blocks)} AND ${Math.max(...blocks)}`
854
+ }
855
+
856
+ /**
857
+ * Resolves the memo of each of a page's transfers from their matching
858
+ * `TransferWithMemo` events, matched back to rows by {@link transferKey}: the
859
+ * decoded text `memo`, and `attribution` (the resolved MPP service name) when
860
+ * the memo is a known service fingerprint. The fingerprint directory is
861
+ * best-effort: a fetch failure leaves a memo unattributed rather than failing
862
+ * the page.
863
+ */
864
+ function memoMap(
865
+ c: Context<App.Environment>,
866
+ options: memoMap.Options,
867
+ ): Promise<Map<string, { attribution?: string | undefined; memo?: string | undefined }>> {
868
+ const { chainId, rows } = options
869
+ const tidx = c.get('getTidx')(chainId)
870
+
871
+ return Timing.time(c, 'transfers_memo', async () => {
872
+ const [memos, fingerprintMap] = await Promise.all([
873
+ tidx.fetch({
874
+ chainId,
875
+ query: `
876
+ SELECT tx_hash, "from", "to", amount, address, memo
877
+ FROM TransferWithMemo
878
+ WHERE ${blockBound(rows)} AND tx_hash IN (${txInList(rows)})
879
+ `,
880
+ signatures: [memoSignature],
881
+ }),
882
+ Mpp.fingerprintMap({ store: c.get('store') }).catch(() => ({})),
883
+ ])
884
+
885
+ const memoByRow = new Map<
886
+ string,
887
+ { attribution?: string | undefined; memo?: string | undefined }
888
+ >()
889
+ for (const row of memos.rows) {
890
+ const rawMemo = Value.toText(row['memo']) as Hex.Hex
891
+ const attribution = Mpp.resolve({ fingerprintMap, memo: rawMemo })
892
+ const memo = memoToString(rawMemo)
893
+ const amount = Value.toIntegerString(row['amount'])
894
+ const txHash = Value.toText(row['tx_hash'])
895
+ const from = Value.toText(row['from'])
896
+ const to = Value.toText(row['to'])
897
+ const token = Value.toText(row['address'])
898
+ if ((attribution || memo) && amount && txHash && from && to && token)
899
+ memoByRow.set(transferKey(txHash, from, to, amount, token), { attribution, memo })
900
+ }
901
+ return memoByRow
902
+ })
903
+ }
904
+
905
+ declare namespace memoMap {
906
+ /** Options for the per-page memo lookup. */
907
+ type Options = {
908
+ /** Source chain id. */
909
+ chainId: z.output<typeof Schema.ChainId>
910
+ /** The page rows; only their `transactionHash` and `blockNumber` are read. */
911
+ rows: readonly { blockNumber: number; transactionHash: string }[]
912
+ }
913
+ }
914
+
915
+ /** A token's contract address and moved amount, matching `SourceToken`'s core. */
916
+ type CrossToken = { address: z.output<typeof Schema.Address>; amount: string }
917
+
918
+ /** A decoded `Transfer` leg used for cross-token classification. */
919
+ type Leg = {
920
+ address: z.output<typeof Schema.Address>
921
+ amount: string
922
+ from: z.output<typeof Schema.Address>
923
+ logIndex: number
924
+ to: z.output<typeof Schema.Address>
925
+ }
926
+
927
+ /** The folded shape of a detected cross-token transfer, anchored on its delivery leg. */
928
+ type CrossTokenBundle = {
929
+ /** Log index of the delivery leg the transfer folds onto. */
930
+ deliveryLogIndex: number
931
+ /** The delivered token and amount (the delivery leg's own event token). */
932
+ destinationToken: CrossToken
933
+ /** The recipient the swapped-out token was forwarded to. */
934
+ recipient: z.output<typeof Schema.Address>
935
+ /** The swapper that funded the transfer through the DEX. */
936
+ sender: z.output<typeof Schema.Address>
937
+ /** The token and amount swapped into the DEX. */
938
+ sourceToken: CrossToken
939
+ /** Log indexes of the legs folded away (the two DEX legs and any fee leg). */
940
+ suppressLogIndexes: number[]
941
+ }
942
+
943
+ /**
944
+ * Classifies a single transaction's `Transfer` legs as a cross-token transfer,
945
+ * or returns `undefined`. A cross-token transfer is the account server's
946
+ * approve+swap+forward multicall: the source token is sent into the DEX, the
947
+ * swapped token comes back to the swapper, and that swapped token is forwarded
948
+ * to a different recipient in the same transaction. The transfer folds onto the
949
+ * delivery (forwarding) leg; the two DEX legs and any fee leg fold away. A swap
950
+ * whose output stays with the swapper (no onward leg) is not a cross-token
951
+ * transfer and is left untouched.
952
+ */
953
+ function classifyCrossToken(legs: readonly Leg[]): CrossTokenBundle | undefined {
954
+ const toDex = legs.find((leg) => Address.isEqual(leg.to, Addresses.stablecoinDex))
955
+ const fromDex = legs.find((leg) => Address.isEqual(leg.from, Addresses.stablecoinDex))
956
+ if (!toDex || !fromDex) return undefined
957
+ const swapper = fromDex.to
958
+ const destTokenAddress = fromDex.address
959
+ const delivery = legs.find(
960
+ (leg) =>
961
+ Address.isEqual(leg.address, destTokenAddress) &&
962
+ Address.isEqual(leg.from, swapper) &&
963
+ !Address.isEqual(leg.to, Addresses.stablecoinDex) &&
964
+ !Address.isEqual(leg.to, swapper) &&
965
+ !Address.isEqual(leg.to, Addresses.feeManager),
966
+ )
967
+ // Require a token change so plain same-token DEX activity is not folded.
968
+ if (!delivery || Address.isEqual(toDex.address, delivery.address)) return undefined
969
+ const suppressLogIndexes = legs
970
+ .filter(
971
+ (leg) =>
972
+ leg.logIndex !== delivery.logIndex &&
973
+ (Address.isEqual(leg.to, Addresses.stablecoinDex) ||
974
+ Address.isEqual(leg.from, Addresses.stablecoinDex) ||
975
+ Address.isEqual(leg.to, Addresses.feeManager)),
976
+ )
977
+ .map((leg) => leg.logIndex)
978
+ return {
979
+ deliveryLogIndex: delivery.logIndex,
980
+ destinationToken: { address: delivery.address, amount: delivery.amount },
981
+ recipient: delivery.to,
982
+ sender: swapper,
983
+ sourceToken: { address: toDex.address, amount: toDex.amount },
984
+ suppressLogIndexes,
985
+ }
986
+ }
987
+
988
+ /** Groups decoded `Transfer` rows by lowercased transaction hash. */
989
+ function legsByTransaction(rows: readonly Record<string, unknown>[]): Map<string, Leg[]> {
990
+ const legsByTx = new Map<string, Leg[]>()
991
+ for (const row of rows) {
992
+ const txHash = Value.toText(row['tx_hash'])
993
+ const from = Schema.Address.safeParse(row['from'])
994
+ const to = Schema.Address.safeParse(row['to'])
995
+ const address = Schema.Address.safeParse(row['address'])
996
+ const amount = Value.toIntegerString(row['value'])
997
+ const logIndex = Value.toNumber(row['log_idx'])
998
+ if (
999
+ txHash &&
1000
+ from.success &&
1001
+ to.success &&
1002
+ address.success &&
1003
+ amount !== undefined &&
1004
+ logIndex !== undefined
1005
+ ) {
1006
+ const legs = legsByTx.get(txHash.toLowerCase()) ?? []
1007
+ legs.push({ address: address.data, amount, from: from.data, logIndex, to: to.data })
1008
+ legsByTx.set(txHash.toLowerCase(), legs)
1009
+ }
1010
+ }
1011
+ return legsByTx
1012
+ }
1013
+
1014
+ /**
1015
+ * Detects cross-token transfers among a page's transactions and returns the
1016
+ * folding plan: `fold` maps each delivery leg (by `${txHash}-${logIndex}`) to
1017
+ * its folded row (swapper, recipient, swapped-in `sourceToken`, delivered
1018
+ * `destinationToken`), and `suppress` is the set of the other bundle legs (the
1019
+ * two DEX legs and any fee leg) to drop from the page.
1020
+ *
1021
+ * Bounded to the page's transactions and block range like {@link memoMap}, so it
1022
+ * discovers the sibling DEX legs even when they fall outside the current page.
1023
+ *
1024
+ * TODO(https://tips.sh/1072): once TIP-1072 lands, cross-token conversions emit
1025
+ * a single self-describing `Convert(user, sourceToken, destinationToken,
1026
+ * backingToken, amount)` event. Replace this extra `Transfer` lookup and the
1027
+ * multi-leg {@link classifyCrossToken} heuristic with a direct `Convert` decode
1028
+ * (and drop the folded-count scan in {@link countScan}).
1029
+ */
1030
+ function crossTokenMap(
1031
+ c: Context<App.Environment>,
1032
+ options: memoMap.Options,
1033
+ ): Promise<{
1034
+ fold: Map<
1035
+ string,
1036
+ {
1037
+ destinationToken: CrossToken
1038
+ recipient: z.output<typeof Schema.Address>
1039
+ sender: z.output<typeof Schema.Address>
1040
+ sourceToken: CrossToken
1041
+ }
1042
+ >
1043
+ suppress: Set<string>
1044
+ }> {
1045
+ const { chainId, rows } = options
1046
+ const tidx = c.get('getTidx')(chainId)
1047
+
1048
+ return Timing.time(c, 'transfers_cross_token', async () => {
1049
+ const result = await tidx.fetch({
1050
+ chainId,
1051
+ query: `
1052
+ SELECT tx_hash, log_idx, "from", "to", address, value
1053
+ FROM Transfer
1054
+ WHERE ${blockBound(rows)} AND tx_hash IN (${txInList(rows)})
1055
+ ORDER BY tx_hash, log_idx
1056
+ `,
1057
+ signatures: [signature],
1058
+ })
1059
+
1060
+ const fold = new Map<
1061
+ string,
1062
+ {
1063
+ destinationToken: CrossToken
1064
+ recipient: z.output<typeof Schema.Address>
1065
+ sender: z.output<typeof Schema.Address>
1066
+ sourceToken: CrossToken
1067
+ }
1068
+ >()
1069
+ const suppress = new Set<string>()
1070
+ for (const [txHash, legs] of legsByTransaction(result.rows)) {
1071
+ const bundle = classifyCrossToken(legs)
1072
+ if (!bundle) continue
1073
+ fold.set(eventKey(txHash, bundle.deliveryLogIndex), {
1074
+ destinationToken: bundle.destinationToken,
1075
+ recipient: bundle.recipient,
1076
+ sender: bundle.sender,
1077
+ sourceToken: bundle.sourceToken,
1078
+ })
1079
+ for (const logIndex of bundle.suppressLogIndexes) suppress.add(eventKey(txHash, logIndex))
1080
+ }
1081
+ return { fold, suppress }
1082
+ })
1083
+ }
1084
+
1085
+ /** Composite `${transactionHash}-${logIndex}` key for a single transfer event. */
1086
+ function eventKey(transactionHash: string, logIndex: number): string {
1087
+ return `${transactionHash}-${logIndex}`.toLowerCase()
1088
+ }
1089
+
1090
+ /**
1091
+ * Attempts to decode a bytes32 memo into a human-readable string. MPP
1092
+ * attribution memos encode a service fingerprint, not text — they are
1093
+ * suppressed from the display memo (and resolved to `attribution` instead).
1094
+ * Shared with the activity feed's transfer entries.
1095
+ */
1096
+ export function memoToString(memo: Hex.Hex): string | undefined {
1097
+ if (Mpp.isAttributionMemo(memo)) return undefined
1098
+ try {
1099
+ const trimmed = Hex.trimLeft(memo)
1100
+ if (trimmed === '0x' || trimmed === '0x0') return undefined
1101
+ return Hex.toString(trimmed)
1102
+ } catch {
1103
+ return undefined
1104
+ }
1105
+ }
1106
+
1107
+ /** Composite match key shared by the `Transfer` row and its `TransferWithMemo`. */
1108
+ function transferKey(
1109
+ txHash: string,
1110
+ from: string,
1111
+ to: string,
1112
+ amount: string,
1113
+ token: string,
1114
+ ): string {
1115
+ return `${txHash}:${from}:${to}:${amount}:${token}`.toLowerCase()
1116
+ }