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,1363 @@
1
+ import { Hono, type Context } from 'hono'
2
+ import { Hex } from 'ox'
3
+ import * as z from 'zod/mini'
4
+
5
+ import type * as App from '../App.js'
6
+ import * as Auth from '../internal/Auth.js'
7
+ import * as Cache from '../internal/Cache.js'
8
+ import * as Cursor from '../internal/Cursor.js'
9
+ import * as OpenApi from '../internal/OpenApi.js'
10
+ import * as Response from '../internal/Response.js'
11
+ import * as Schema from '../internal/Schema.js'
12
+ import * as Store from '../internal/Store.js'
13
+ import type * as Tidx from '../internal/Tidx.js'
14
+ import * as Timing from '../internal/Timing.js'
15
+ import * as Ttl from '../internal/Ttl.js'
16
+ import * as Value from '../internal/Value.js'
17
+ import * as VerifiedTokens from '../internal/VerifiedTokens.js'
18
+ import * as Receipts from './receipts.js'
19
+ import * as Tokens from './tokens.js'
20
+
21
+ /** Example transaction hash surfaced in OpenAPI docs. */
22
+ const exampleHash = '0x515801d7f9a5ac705e793e85904c9c69b3f1694b465cc8fb6ba3f0298dc82665'
23
+
24
+ /** A decoded transaction type name. */
25
+ type TransactionType =
26
+ | 'legacy'
27
+ | 'eip2930'
28
+ | 'eip1559'
29
+ | 'eip4844'
30
+ | 'eip7702'
31
+ | 'tempo'
32
+ | 'unknown'
33
+
34
+ /**
35
+ * Maps a transaction `type` byte (as a number) to its human-readable name. The
36
+ * raw `0x…` byte is always preserved under `rpc.type`.
37
+ */
38
+ const transactionTypeByNumber: Record<number, TransactionType> = {
39
+ 0: 'legacy',
40
+ 1: 'eip2930',
41
+ 2: 'eip1559',
42
+ 3: 'eip4844',
43
+ 4: 'eip7702',
44
+ 118: 'tempo',
45
+ }
46
+
47
+ /** Decodes a transaction `type` (hex string or number) into its name. */
48
+ export function decodeTransactionType(value: unknown): TransactionType {
49
+ const byte = typeof value === 'number' ? value : Value.hexToNumber(value)
50
+ return (byte !== undefined && transactionTypeByNumber[byte]) || 'unknown'
51
+ }
52
+
53
+ /** Zod schemas owned by the transaction handlers. */
54
+ export namespace schema {
55
+ // TODO(ox/zod): replace with the zod schema for `ox`'s `AccessList.Item` once
56
+ // ox ships a `zod` entrypoint.
57
+ /** One EIP-2930 access list entry (RPC format). */
58
+ export const AccessListItem = z
59
+ .looseObject({
60
+ address: Schema.Address.check(
61
+ z.describe('The account whose storage slots are listed in this access-list entry.'),
62
+ ),
63
+ storageKeys: z
64
+ .array(Schema.Hash)
65
+ .check(z.describe('The storage slot keys the transaction plans to access.')),
66
+ })
67
+ .check(
68
+ z.describe('One EIP-2930 access-list entry that predeclares an account and storage slots.'),
69
+ )
70
+
71
+ // TODO(ox/zod): replace with the zod schema for the RPC `Call` in `ox/tempo`'s
72
+ // `TxEnvelopeTempo`/`Transaction` types once ox ships a `zod` entrypoint.
73
+ /** One call within a Tempo account-abstraction transaction (RPC format). */
74
+ export const Call = z
75
+ .looseObject({
76
+ data: z
77
+ .nullish(Schema.Hex)
78
+ .check(
79
+ z.describe(
80
+ 'Call data for this call; this is the same bytes as `input` when both are present.',
81
+ ),
82
+ z.meta({ examples: ['0xdeadbeef'] }),
83
+ ),
84
+ input: z
85
+ .nullish(Schema.Hex)
86
+ .check(
87
+ z.describe('Call data sent to the contract, as `0x`-prefixed bytes.'),
88
+ z.meta({ examples: ['0xdeadbeef'] }),
89
+ ),
90
+ to: z
91
+ .nullish(Schema.Address)
92
+ .check(
93
+ z.describe('The address this call targets, or `null` when the call creates a contract.'),
94
+ ),
95
+ value: z
96
+ .nullish(Schema.Quantity)
97
+ .check(z.describe('Native value sent with this call, as a hex quantity.')),
98
+ })
99
+ .check(z.describe('One operation inside a Tempo account-abstraction transaction.'))
100
+
101
+ // TODO(ox/zod): replace with the zod schema for `ox/tempo`'s
102
+ // `SignatureEnvelope.Rpc` (the secp256k1 / p256 / webAuthn / keychain union)
103
+ // once ox ships a `zod` entrypoint.
104
+ /**
105
+ * A signature envelope (RPC format), passed through verbatim. Every field is
106
+ * optional because the envelope is a union (secp256k1 / p256 / webAuthn /
107
+ * keychain) whose shape — including whether a `type` discriminator is present —
108
+ * varies; the loose object preserves whatever the RPC returns.
109
+ */
110
+ export const SignatureEnvelope = z
111
+ .looseObject({
112
+ type: z
113
+ .optional(z.string())
114
+ .check(
115
+ z.describe(
116
+ 'The signature scheme used by this envelope, such as `secp256k1`, `p256`, or `keychain` when the RPC includes it.',
117
+ ),
118
+ z.meta({ examples: ['secp256k1'] }),
119
+ ),
120
+ })
121
+ .check(z.describe('The signature envelope exactly as returned by JSON-RPC.'))
122
+
123
+ /** The verbatim JSON-RPC payloads, surfaced under each resource's `rpc` field. */
124
+ export namespace Rpc {
125
+ // TODO(ox/zod): replace with the zod schema for `ox/tempo`'s
126
+ // `Transaction.Rpc` union once ox ships a `zod` entrypoint.
127
+ /** A JSON-RPC transaction, passed through verbatim. */
128
+ export const Transaction = z
129
+ .looseObject({
130
+ aaAuthorizationList: z
131
+ .optional(z.array(z.looseObject({})))
132
+ .check(
133
+ z.describe('Tempo account-abstraction authorizations attached to the transaction.'),
134
+ ),
135
+ accessList: z
136
+ .optional(z.array(AccessListItem))
137
+ .check(
138
+ z.describe(
139
+ 'EIP-2930 access list that predeclares accounts and storage slots for the transaction.',
140
+ ),
141
+ ),
142
+ authorizationList: z
143
+ .optional(z.array(z.looseObject({})))
144
+ .check(z.describe('EIP-7702 authorizations attached to the transaction.')),
145
+ blobVersionedHashes: z
146
+ .optional(z.array(Schema.Hash))
147
+ .check(z.describe('Versioned blob hashes for EIP-4844 blob data.')),
148
+ blockHash: z
149
+ .nullable(Schema.Hash)
150
+ .check(
151
+ z.describe('The block hash once included, or `null` while the transaction is pending.'),
152
+ ),
153
+ blockNumber: z
154
+ .nullable(Schema.Quantity)
155
+ .check(
156
+ z.describe(
157
+ 'The block number once included, or `null` while the transaction is pending.',
158
+ ),
159
+ ),
160
+ blockTimestamp: z
161
+ .nullish(Schema.Quantity)
162
+ .check(z.describe('The block timestamp for the block that included this transaction.')),
163
+ calls: z
164
+ .optional(
165
+ z.union([
166
+ z.array(Call).check(
167
+ z.meta({
168
+ examples: [[{ data: '0xdeadbeef', input: '0xdeadbeef', to: null, value: '0x0' }]],
169
+ }),
170
+ ),
171
+ z.null().check(z.meta({ examples: [null] })),
172
+ ]),
173
+ )
174
+ .check(
175
+ z.describe('Decoded calls for Tempo account-abstraction transactions.'),
176
+ z.meta({
177
+ examples: [[{ data: '0xdeadbeef', input: '0xdeadbeef', to: null, value: '0x0' }]],
178
+ }),
179
+ ),
180
+ chainId: z
181
+ .optional(Schema.Quantity)
182
+ .check(
183
+ z.describe(
184
+ 'The chain ID that the transaction is valid on; legacy transactions may omit it.',
185
+ ),
186
+ ),
187
+ feePayerSignature: z
188
+ .nullish(SignatureEnvelope)
189
+ .check(
190
+ z.describe('Signature from the account that paid the transaction fee.'),
191
+ z.meta({ examples: [{ type: 'secp256k1' }] }),
192
+ ),
193
+ feeToken: z
194
+ .nullish(Schema.Address)
195
+ .check(
196
+ z.describe(
197
+ 'The fee token requested by the transaction; Tempo fees are paid in USD stablecoins such as `pathUSD`.',
198
+ ),
199
+ ),
200
+ from: Schema.Address.check(
201
+ z.describe('The address that submitted or authorized the transaction.'),
202
+ ),
203
+ gas: Schema.Quantity.check(z.describe('Maximum gas the transaction is allowed to use.')),
204
+ gasPrice: z
205
+ .optional(Schema.Quantity)
206
+ .check(
207
+ z.describe(
208
+ 'Gas price for legacy-style transactions, expressed as a decimal string when humanized.',
209
+ ),
210
+ ),
211
+ hash: Schema.Hash.check(
212
+ z.describe(
213
+ 'A 32-byte hash, `0x`-prefixed and returned in lowercase, that identifies this transaction.',
214
+ ),
215
+ ),
216
+ input: z
217
+ .optional(Schema.Hex)
218
+ .check(
219
+ z.describe('Call data for non-Tempo transaction formats, as `0x`-prefixed bytes.'),
220
+ z.meta({ examples: ['0xdeadbeef'] }),
221
+ ),
222
+ keyAuthorization: z
223
+ .nullish(z.looseObject({}))
224
+ .check(
225
+ z.describe('Key authorization data attached to this Tempo transaction.'),
226
+ z.meta({ examples: [{}] }),
227
+ ),
228
+ maxFeePerBlobGas: z
229
+ .optional(Schema.Quantity)
230
+ .check(z.describe('Maximum fee per blob gas for EIP-4844 blob data.')),
231
+ maxFeePerGas: z
232
+ .optional(Schema.Quantity)
233
+ .check(z.describe('Maximum total fee per gas the sender is willing to pay.')),
234
+ maxPriorityFeePerGas: z
235
+ .optional(Schema.Quantity)
236
+ .check(z.describe('Maximum priority fee per gas for EIP-1559-style transactions.')),
237
+ nonce: Schema.Quantity.check(
238
+ z.describe('Nonce from the sender account that orders and de-duplicates transactions.'),
239
+ ),
240
+ nonceKey: z
241
+ .optional(Schema.Quantity)
242
+ .check(z.describe('Tempo two-dimensional nonce key used to group nonce sequences.')),
243
+ r: z
244
+ .optional(Schema.Quantity)
245
+ .check(z.describe('The `r` value from the transaction ECDSA signature.')),
246
+ s: z
247
+ .optional(Schema.Quantity)
248
+ .check(z.describe('The `s` value from the transaction ECDSA signature.')),
249
+ signature: z
250
+ .optional(SignatureEnvelope)
251
+ .check(z.describe('Signature from the sender authorizing this transaction.')),
252
+ to: z
253
+ .nullish(Schema.Address)
254
+ .check(
255
+ z.describe('The recipient address, or `null` when the transaction creates a contract.'),
256
+ ),
257
+ transactionIndex: z
258
+ .nullable(Schema.Quantity)
259
+ .check(z.describe('The transaction position within its block, or `null` while pending.')),
260
+ type: Schema.Hex.check(
261
+ z.describe('Raw transaction type byte, such as `0x2` for EIP-1559 or `0x76` for Tempo.'),
262
+ z.meta({ examples: ['0x2'] }),
263
+ ),
264
+ v: z
265
+ .optional(Schema.Quantity)
266
+ .check(z.describe('The `v` recovery value from the transaction ECDSA signature.')),
267
+ validAfter: z
268
+ .nullish(Schema.Quantity)
269
+ .check(
270
+ z.describe('Earliest Unix timestamp when this Tempo transaction may be included.'),
271
+ z.meta({ examples: ['0x0'] }),
272
+ ),
273
+ validBefore: z
274
+ .nullish(Schema.Quantity)
275
+ .check(
276
+ z.describe('Latest Unix timestamp when this Tempo transaction may be included.'),
277
+ z.meta({ examples: ['0xffffffff'] }),
278
+ ),
279
+ value: z
280
+ .optional(Schema.Quantity)
281
+ .check(z.describe('Native value sent by non-Tempo transaction formats.')),
282
+ yParity: z
283
+ .optional(Schema.Quantity)
284
+ .check(z.describe('The y-parity value from the transaction signature.')),
285
+ })
286
+ .check(z.describe('The transaction exactly as returned by JSON-RPC.'))
287
+ }
288
+
289
+ /**
290
+ * Metadata for a transaction: the verbatim JSON-RPC transaction (always
291
+ * present) plus resources embedded on demand via `include`.
292
+ *
293
+ * The `feeToken` and `receipt` schemas are wrapped in `z.lazy` because
294
+ * `tokens.ts`/`receipts.ts` and `transactions.ts` mutually reference each
295
+ * other's schemas; deferring these lookups avoids the import-cycle crash
296
+ * during module init.
297
+ */
298
+ export const TransactionMeta = z
299
+ .object({
300
+ feeToken: z
301
+ .optional(z.lazy(() => Tokens.schema.getToken.Response))
302
+ .check(z.describe('Fee-token details included only when you request `include=feeToken`.')),
303
+ receipt: z
304
+ .optional(z.lazy(() => Receipts.schema.Receipt))
305
+ .check(
306
+ z.describe('The transaction outcome included only when you request `include=receipt`.'),
307
+ ),
308
+ rpc: Rpc.Transaction.check(z.describe('The original JSON-RPC transaction payload.')),
309
+ })
310
+ .check(
311
+ z.describe(
312
+ 'Metadata for this transaction: the original JSON-RPC payload plus any requested `include` resources.',
313
+ ),
314
+ )
315
+
316
+ /** Decoded transaction `type`; the raw `0x…` byte stays under `rpc.type`. */
317
+ export const TransactionType = z
318
+ .enum(['legacy', 'eip2930', 'eip1559', 'eip4844', 'eip7702', 'tempo', 'unknown'])
319
+ .check(
320
+ z.describe('Human-readable transaction type decoded from the raw JSON-RPC `type` byte.'),
321
+ z.meta({ examples: ['eip1559'] }),
322
+ )
323
+
324
+ /** One humanized call within a Tempo account-abstraction transaction. */
325
+ export const HumanCall = z
326
+ .object({
327
+ data: z
328
+ .optional(Schema.Hex)
329
+ .check(
330
+ z.describe('Call data sent to the contract, as `0x`-prefixed bytes.'),
331
+ z.meta({ examples: ['0xdeadbeef'] }),
332
+ ),
333
+ to: z
334
+ .nullable(Schema.Address)
335
+ .check(z.describe('The address this call targets, or `null` when it creates a contract.')),
336
+ })
337
+ .check(z.describe('One decoded call inside a Tempo account-abstraction transaction.'))
338
+
339
+ /**
340
+ * A humanized transaction: decoded `type`, decimal-string amounts, ISO 8601
341
+ * timestamps, and plain-number block fields. The verbatim JSON-RPC transaction
342
+ * is always available under `meta.rpc`.
343
+ */
344
+ export const Transaction = z
345
+ .object({
346
+ blockHash: z
347
+ .nullable(Schema.Hash)
348
+ .check(
349
+ z.describe('The block hash once included, or `null` while the transaction is pending.'),
350
+ ),
351
+ blockNumber: z
352
+ .nullable(z.number().check(z.int(), z.nonnegative()))
353
+ .check(
354
+ z.describe('The block number once included, or `null` while the transaction is pending.'),
355
+ z.meta({ examples: [23456789] }),
356
+ ),
357
+ calls: z
358
+ .optional(z.array(HumanCall))
359
+ .check(
360
+ z.describe('Decoded calls for Tempo account-abstraction transactions.'),
361
+ z.meta({ examples: [[{ data: '0xdeadbeef', to: null }]] }),
362
+ ),
363
+ chainId: z
364
+ .optional(z.number().check(z.int(), z.nonnegative()))
365
+ .check(
366
+ z.describe('The chain ID that this transaction belongs to.'),
367
+ z.meta({ examples: [4217] }),
368
+ ),
369
+ feeToken: z
370
+ .optional(Schema.Address)
371
+ .check(
372
+ z.describe(
373
+ 'The fee token requested by the transaction; Tempo fees are paid in USD stablecoins such as `pathUSD`.',
374
+ ),
375
+ ),
376
+ gas: z
377
+ .number()
378
+ .check(
379
+ z.int(),
380
+ z.nonnegative(),
381
+ z.describe('Maximum gas the transaction is allowed to use.'),
382
+ z.meta({ examples: [21000] }),
383
+ ),
384
+ gasPrice: z
385
+ .optional(Schema.DecimalString)
386
+ .check(
387
+ z.describe(
388
+ 'Gas price for legacy-style transactions, expressed as a decimal string when humanized.',
389
+ ),
390
+ ),
391
+ hash: Schema.Hash.check(
392
+ z.describe(
393
+ 'A 32-byte hash, `0x`-prefixed and returned in lowercase, that identifies this transaction.',
394
+ ),
395
+ ),
396
+ id: Schema.Hash.check(
397
+ z.describe('Stable resource ID for this API response; it is the transaction hash.'),
398
+ ),
399
+ input: z
400
+ .optional(Schema.Hex)
401
+ .check(
402
+ z.describe('Call data for non-Tempo transaction formats, as `0x`-prefixed bytes.'),
403
+ z.meta({ examples: ['0xdeadbeef'] }),
404
+ ),
405
+ maxFeePerGas: z
406
+ .optional(Schema.DecimalString)
407
+ .check(z.describe('Maximum total fee per gas the sender is willing to pay.')),
408
+ maxPriorityFeePerGas: z
409
+ .optional(Schema.DecimalString)
410
+ .check(z.describe('Maximum priority fee per gas for EIP-1559-style transactions.')),
411
+ meta: TransactionMeta.check(
412
+ z.describe('The original JSON-RPC payload plus any resources requested with `include`.'),
413
+ ),
414
+ nonce: z
415
+ .number()
416
+ .check(
417
+ z.int(),
418
+ z.nonnegative(),
419
+ z.describe('Nonce from the sender account that orders and de-duplicates transactions.'),
420
+ z.meta({ examples: [0] }),
421
+ ),
422
+ nonceKey: z
423
+ .optional(Schema.Hex)
424
+ .check(
425
+ z.describe('Tempo two-dimensional nonce key used by Tempo transactions.'),
426
+ z.meta({ examples: ['0x0'] }),
427
+ ),
428
+ recipient: z
429
+ .nullable(Schema.Address)
430
+ .check(
431
+ z.describe('The recipient address, or `null` when the transaction creates a contract.'),
432
+ ),
433
+ sender: Schema.Address.check(
434
+ z.describe('The address that submitted or authorized the transaction.'),
435
+ ),
436
+ timestamp: z
437
+ .nullable(z.iso.datetime())
438
+ .check(
439
+ z.describe(
440
+ 'The block timestamp as ISO 8601, or `null` while the transaction is pending.',
441
+ ),
442
+ z.meta({ examples: ['2024-01-01T00:00:00Z'] }),
443
+ ),
444
+ transactionIndex: z
445
+ .nullable(z.number().check(z.int(), z.nonnegative()))
446
+ .check(
447
+ z.describe('The transaction position within its block, or `null` while pending.'),
448
+ z.meta({ examples: [0] }),
449
+ ),
450
+ type: TransactionType,
451
+ validAfter: z
452
+ .nullish(z.iso.datetime())
453
+ .check(
454
+ z.describe('Earliest ISO 8601 time when this Tempo transaction may be included.'),
455
+ z.meta({ examples: ['2024-01-01T00:00:00Z'] }),
456
+ ),
457
+ validBefore: z
458
+ .nullish(z.iso.datetime())
459
+ .check(
460
+ z.describe('Latest ISO 8601 time when this Tempo transaction may be included.'),
461
+ z.meta({ examples: ['2024-01-01T00:00:00Z'] }),
462
+ ),
463
+ value: Schema.DecimalString.check(
464
+ z.describe('Native value transferred by this transaction, as a decimal string.'),
465
+ ),
466
+ })
467
+ .check(
468
+ z.describe(
469
+ 'A transaction formatted for API clients, with decoded fields and the original JSON-RPC payload under `meta.rpc`.',
470
+ ),
471
+ )
472
+
473
+ /** Schemas for the getTransaction operation. */
474
+ export namespace getTransaction {
475
+ /** Path parameters for transaction requests. */
476
+ export const Params = z
477
+ .object({
478
+ transactionHash: Schema.Hash.check(
479
+ z.describe(
480
+ 'A 32-byte hash, `0x`-prefixed and returned in lowercase, that identifies this transaction.',
481
+ ),
482
+ z.meta({ examples: [exampleHash] }),
483
+ ),
484
+ })
485
+ .check(z.describe('Path parameters used to look up a transaction.'))
486
+
487
+ /** Optional related resource that callers opt into via `include`. */
488
+ export const Include = z
489
+ .enum(['feeToken', 'receipt'])
490
+ .check(z.describe('Related resource you can opt into with the `include` query parameter.'))
491
+
492
+ /**
493
+ * Parses a comma-separated `include` query value into a list of optional
494
+ * resources to embed. Extra lookups (e.g. fee-token metadata, the receipt)
495
+ * only run when explicitly requested, keeping the base transaction response
496
+ * fast.
497
+ */
498
+ export const includeQuery = Schema.includeQuery(
499
+ Include,
500
+ 'Comma-separated related resources to include, such as `feeToken,receipt`.',
501
+ )
502
+
503
+ /** Query parameters for transaction requests. */
504
+ export const Query = z
505
+ .strictObject({
506
+ chainId: Schema.ChainIdQuery,
507
+ include: includeQuery,
508
+ })
509
+ .check(z.describe('Query parameters for looking up a transaction.'))
510
+
511
+ /** A humanized transaction, with the verbatim JSON-RPC payload under `rpc`. */
512
+ export const Response = Transaction
513
+ }
514
+
515
+ /** Schemas for the getTransactions (list) operation. */
516
+ export namespace getTransactions {
517
+ /**
518
+ * Optional resources for the transaction list: the per-row `feeToken` /
519
+ * `receipt` embeds plus the response-wide capped `totalCount`. The detail
520
+ * route keeps its own {@link getTransaction.Include}.
521
+ */
522
+ export const Include = z
523
+ .enum(['feeToken', 'receipt', 'totalCount'])
524
+ .check(
525
+ z.describe('Related resources to include in each transaction row or in response metadata.'),
526
+ )
527
+
528
+ /** Parses the comma-separated `include` value for the transaction list. */
529
+ export const includeQuery = Schema.includeQuery(
530
+ Include,
531
+ 'Comma-separated related resources to include, such as `feeToken,receipt,totalCount`.',
532
+ )
533
+
534
+ /** Query parameters for transaction list requests. */
535
+ export const Query = z
536
+ .strictObject({
537
+ address: z
538
+ .optional(Schema.Address)
539
+ .check(
540
+ z.describe(
541
+ 'Filter to transactions where this address is either the sender or recipient.',
542
+ ),
543
+ ),
544
+ 'blockNumber.from': Schema.blockNumberBound('transactions', 'from'),
545
+ 'blockNumber.to': Schema.blockNumberBound('transactions', 'to'),
546
+ chainId: Schema.ChainIdQuery,
547
+ cursor: Schema.Cursor,
548
+ feePayer: z
549
+ .optional(Schema.Address)
550
+ .check(z.describe('Filter to transactions whose fee payer is this address.')),
551
+ feeToken: z
552
+ .optional(Schema.TokenAddress)
553
+ .check(z.describe('Filter to transactions whose fee was paid in this token address.')),
554
+ include: includeQuery,
555
+ limit: Schema.Limit,
556
+ order: Schema.Order,
557
+ page: Schema.Page,
558
+ recipient: z
559
+ .optional(Schema.Address)
560
+ .check(z.describe('Filter to transactions sent to this recipient address.')),
561
+ sender: z
562
+ .optional(Schema.Address)
563
+ .check(z.describe('Filter to transactions sent by this sender address.')),
564
+ status: z
565
+ .optional(z.enum(['success', 'reverted']))
566
+ .check(
567
+ z.describe('Filter to transactions whose receipt shows this execution status.'),
568
+ z.meta({ examples: ['success'] }),
569
+ ),
570
+ 'timestamp.from': Schema.timestampBound('transactions', 'from'),
571
+ 'timestamp.to': Schema.timestampBound('transactions', 'to'),
572
+ })
573
+ .check(...Schema.pageChecks(), z.describe('Query parameters for listing transactions.'))
574
+
575
+ /**
576
+ * Page of transactions. Each item reuses the humanized transaction returned
577
+ * by `GET /transactions/:transactionHash`, so the list and detail endpoints
578
+ * expose one identical shape. Indexed list rows omit fields the indexer does
579
+ * not store (signatures, access list), reflected in their `rpc` payload.
580
+ */
581
+ export const Response = z
582
+ .object({
583
+ data: z.array(getTransaction.Response).check(z.describe('Transactions on this page.')),
584
+ meta: z
585
+ .optional(Schema.CountMeta)
586
+ .check(
587
+ z.describe('Response-level metadata requested with `include`, such as `totalCount`.'),
588
+ ),
589
+ nextCursor: Schema.NextCursor,
590
+ })
591
+ .check(z.describe('A page of transactions ordered by block number and transaction position.'))
592
+ }
593
+ }
594
+ /** Creates transaction handlers. */
595
+ export function transactions() {
596
+ return new Hono<App.Environment>()
597
+ .get(
598
+ '/',
599
+ Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }),
600
+ OpenApi.validate('query', schema.getTransactions.Query, {
601
+ code: 'query_invalid',
602
+ message: 'Invalid query parameters',
603
+ }),
604
+ OpenApi.describeRoute({
605
+ description:
606
+ 'List transactions across Tempo, with filters for addresses, blocks, timestamps, fees, and status.',
607
+ operationId: 'getTransactions',
608
+ responses: OpenApi.responses({
609
+ errors: { 502: 'Could not read indexed transaction data from TIDX.' },
610
+ success: {
611
+ description: 'A page of transactions.',
612
+ schema: schema.getTransactions.Response,
613
+ },
614
+ }),
615
+ summary: 'List transactions',
616
+ tags: ['Transactions'],
617
+ }),
618
+ Cache.response<App.Environment>({
619
+ cacheControl: Cache.policies.feed,
620
+ name: 'tempo-api:transactions:v1',
621
+ key: (c) => Cache.urlKey(c, schema.getTransactions.Query),
622
+ }),
623
+ async (c) => {
624
+ if (Auth.narrowAccess) return Auth.paidAccessError(c)
625
+ if (OpenApi.narrowValidation)
626
+ return OpenApi.validationError(c, {
627
+ code: 'query_invalid',
628
+ message: 'Invalid query parameters',
629
+ })
630
+
631
+ const query = c.req.valid('query')
632
+ const chainId = query.chainId ?? c.get('chainId')
633
+
634
+ // A cursor page is anchored below the head, so its contents are
635
+ // append-only and effectively immutable; cache it aggressively. The head
636
+ // page (no cursor) keeps the route's short `feed` default.
637
+ if (query.cursor !== undefined) Cache.setPolicy(c, Cache.policies.immutable)
638
+
639
+ try {
640
+ // `totalCount` is opt-in and shares the page's filters; run it
641
+ // concurrently with the page. Best-effort: a count failure omits
642
+ // `meta` rather than failing the page.
643
+ const countPromise = query.include.includes('totalCount')
644
+ ? countTransactions(c, {
645
+ address: query.address,
646
+ chainId,
647
+ feePayer: query.feePayer,
648
+ feeToken: query.feeToken,
649
+ fromBlock: query['blockNumber.from'],
650
+ fromTimestamp: query['timestamp.from'],
651
+ recipient: query.recipient,
652
+ sender: query.sender,
653
+ status: query.status,
654
+ toBlock: query['blockNumber.to'],
655
+ toTimestamp: query['timestamp.to'],
656
+ }).catch(() => undefined)
657
+ : undefined
658
+
659
+ const transactions = await listTransactions(c, {
660
+ address: query.address,
661
+ chainId,
662
+ cursor: query.cursor,
663
+ feePayer: query.feePayer,
664
+ feeToken: query.feeToken,
665
+ fromBlock: query['blockNumber.from'],
666
+ fromTimestamp: query['timestamp.from'],
667
+ include: query.include.filter(
668
+ (resource): resource is z.output<typeof schema.getTransaction.Include> =>
669
+ resource !== 'totalCount',
670
+ ),
671
+ limit: query.limit,
672
+ order: query.order,
673
+ page: query.page,
674
+ recipient: query.recipient,
675
+ sender: query.sender,
676
+ status: query.status,
677
+ toBlock: query['blockNumber.to'],
678
+ toTimestamp: query['timestamp.to'],
679
+ })
680
+
681
+ const meta = countPromise ? await countPromise : undefined
682
+ return c.json(
683
+ Response.validated(schema.getTransactions.Response, {
684
+ ...transactions,
685
+ ...(meta ? { meta } : {}),
686
+ }),
687
+ 200,
688
+ )
689
+ } catch (cause) {
690
+ return Response.upstream(c, cause)
691
+ }
692
+ },
693
+ )
694
+ .get(
695
+ '/:transactionHash',
696
+ Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }),
697
+ OpenApi.validate('param', schema.getTransaction.Params, {
698
+ code: 'transaction_invalid',
699
+ message: 'Invalid transaction hash',
700
+ }),
701
+ OpenApi.validate('query', schema.getTransaction.Query, {
702
+ code: 'query_invalid',
703
+ message: 'Invalid query parameters',
704
+ }),
705
+ OpenApi.describeRoute({
706
+ description: 'Get one transaction by its 32-byte transaction hash.',
707
+ operationId: 'getTransaction',
708
+ responses: OpenApi.responses({
709
+ errors: {
710
+ 404: {
711
+ description: 'No transaction was found for that hash.',
712
+ codes: ['transaction_not_found'],
713
+ },
714
+ 502: 'Could not read transaction data from the upstream JSON-RPC node.',
715
+ },
716
+ success: {
717
+ description: 'A single transaction with decoded fields and original JSON-RPC data.',
718
+ schema: schema.getTransaction.Response,
719
+ },
720
+ }),
721
+ summary: 'Get a transaction by hash',
722
+ tags: ['Transactions'],
723
+ }),
724
+ Cache.response<App.Environment>({
725
+ // Default to no-store: a pending (or not-found) transaction must not be
726
+ // cached, or it would mask the mined result. The handler upgrades a mined
727
+ // transaction to `immutable` below.
728
+ cacheControl: Cache.policies.noStore,
729
+ name: 'tempo-api:transactions:v1',
730
+ key: (c) => Cache.urlKey(c, schema.getTransaction.Query),
731
+ }),
732
+ async (c) => {
733
+ if (Auth.narrowAccess) return Auth.paidAccessError(c)
734
+ if (OpenApi.narrowValidation)
735
+ return OpenApi.validationError(c, {
736
+ code: 'transaction_invalid',
737
+ message: 'Invalid request parameters',
738
+ })
739
+ if (OpenApi.narrowValidation)
740
+ return OpenApi.validationError(c, {
741
+ code: 'query_invalid',
742
+ message: 'Invalid request parameters',
743
+ })
744
+
745
+ const { transactionHash } = c.req.valid('param')
746
+ const query = c.req.valid('query')
747
+ const chainId = query.chainId ?? c.get('chainId')
748
+ const client = c.get('getClient')(chainId)
749
+ // `Schema.Hash` lowercases to a plain string; re-narrow to the hex
750
+ // template literal viem requires at the RPC boundary.
751
+ const hash = transactionHash as Hex.Hex
752
+
753
+ try {
754
+ // Fetch the JSON-RPC transaction; a `null` result means the
755
+ // transaction is unknown rather than an error. The verbatim payload is
756
+ // surfaced under `rpc`, with humanized fields layered on top. Unmined
757
+ // lookups (pending or unknown hash) are deliberately never cached —
758
+ // confirmation polling must observe the mined result the moment the
759
+ // node does.
760
+ const rpc = await Timing.time(c, 'transaction', () =>
761
+ client.request({ method: 'eth_getTransactionByHash', params: [hash] }),
762
+ )
763
+ if (!rpc)
764
+ return Response.error(c, {
765
+ code: 'transaction_not_found',
766
+ message: 'Transaction not found',
767
+ status: 404,
768
+ })
769
+
770
+ let data = humanizeTransaction(Response.validated(schema.Rpc.Transaction, rpc))
771
+
772
+ // A mined transaction is immutable, so it can be cached aggressively; a
773
+ // pending one (no block yet) keeps the route's default no-store policy.
774
+ if (data.blockNumber !== null) Cache.setPolicy(c, Cache.policies.immutable)
775
+
776
+ // The receipt is opt-in: fetching it is an extra RPC, so it only runs
777
+ // when requested via `include=receipt` and the transaction is mined.
778
+ // A null receipt (still pending) is simply omitted.
779
+ if (query.include.includes('receipt') && data.blockNumber !== null) {
780
+ const receipt = await Timing.time(c, 'receipt', () =>
781
+ client.request({ method: 'eth_getTransactionReceipt', params: [hash] }),
782
+ )
783
+ if (receipt) {
784
+ const humanized = Receipts.humanizeReceipt(
785
+ Response.validated(Receipts.schema.Rpc.Receipt, receipt),
786
+ )
787
+ data = { ...data, meta: { ...data.meta, receipt: humanized } }
788
+ }
789
+ }
790
+
791
+ // Fee-token metadata is opt-in: resolving it is an extra RPC, so it
792
+ // only runs when requested via `include=feeToken` and the transaction
793
+ // declares a fee token (Tempo AA transactions).
794
+ const feeToken = data.feeToken
795
+ if (query.include.includes('feeToken') && feeToken !== undefined) {
796
+ const token = await Timing.time(c, 'fee_token', () =>
797
+ Tokens.resolveToken(c, { address: feeToken, chainId }).catch(() => undefined),
798
+ )
799
+ if (token) data = { ...data, meta: { ...data.meta, feeToken: token } }
800
+ }
801
+
802
+ return c.json(Response.validated(schema.getTransaction.Response, data), 200)
803
+ } catch (cause) {
804
+ return Response.upstream(c, cause)
805
+ }
806
+ },
807
+ )
808
+ }
809
+
810
+ /**
811
+ * Lists humanized transactions and embeds opt-in `include` resources
812
+ * (`feeToken`, `receipt`). A `status` filter routes the page through the
813
+ * receipts list instead of the `txs` scan, since execution status lives on
814
+ * receipts.
815
+ */
816
+ export async function listTransactions(
817
+ c: Context<App.Environment>,
818
+ options: listTransactions.Options,
819
+ ) {
820
+ const { include, ...rest } = options
821
+
822
+ // `status` filters on the receipt's execution status, which `txs` does not
823
+ // store: page the receipts list (which already supports `status`) and hydrate
824
+ // each receipt's underlying transaction. The receipts `(block_num, tx_idx)`
825
+ // keyset cursor spans the same domain as the transactions `(block_num, idx)`
826
+ // cursor, so cursors stay interchangeable between the two paths.
827
+ const receiptPage =
828
+ rest.status === undefined
829
+ ? undefined
830
+ : await Receipts.getReceipts(c, {
831
+ address: rest.address,
832
+ chainId: rest.chainId,
833
+ cursor: rest.cursor,
834
+ feePayer: rest.feePayer,
835
+ feeToken: rest.feeToken,
836
+ fromBlock: rest.fromBlock,
837
+ fromTimestamp: rest.fromTimestamp,
838
+ limit: rest.limit,
839
+ order: rest.order,
840
+ page: rest.page,
841
+ recipient: rest.recipient,
842
+ sender: rest.sender,
843
+ status: rest.status,
844
+ toBlock: rest.toBlock,
845
+ toTimestamp: rest.toTimestamp,
846
+ })
847
+ const page = receiptPage
848
+ ? {
849
+ data: await transactionsForReceipts(c, {
850
+ chainId: rest.chainId,
851
+ receipts: receiptPage.data,
852
+ }),
853
+ nextCursor: receiptPage.nextCursor,
854
+ }
855
+ : await getTransactions(c, rest)
856
+
857
+ let data = page.data
858
+ if (include.includes('receipt') && data.length > 0) {
859
+ const receipts = await Receipts.embedReceipts(c, {
860
+ chainId: rest.chainId,
861
+ receipts: receiptPage?.data,
862
+ rows: data,
863
+ })
864
+ data = data.map((row) => {
865
+ const receipt = receipts.get(row.hash.toLowerCase())
866
+ return receipt ? { ...row, meta: { ...row.meta, receipt } } : row
867
+ })
868
+ }
869
+ if (include.includes('feeToken')) data = await embedFeeTokens(c, data, rest.chainId)
870
+ return { ...page, data }
871
+ }
872
+
873
+ export declare namespace listTransactions {
874
+ type Options = getTransactions.Options & {
875
+ /** Optional related resources to embed (e.g. `feeToken`, `receipt`). */
876
+ include: readonly z.output<typeof schema.getTransaction.Include>[]
877
+ }
878
+ }
879
+
880
+ /**
881
+ * Fetches the underlying transactions for a page of receipts in one indexed
882
+ * `txs` query and returns them humanized, ordered to match the receipt page.
883
+ * Receipts whose transaction row is missing (not yet indexed) are skipped.
884
+ * Backs the `status`-filtered transactions list, which pages receipts first.
885
+ */
886
+ async function transactionsForReceipts(
887
+ c: Context<App.Environment>,
888
+ options: {
889
+ chainId: z.output<typeof Schema.ChainId>
890
+ receipts: readonly z.output<typeof Receipts.schema.Receipt>[]
891
+ },
892
+ ): Promise<z.output<typeof schema.Transaction>[]> {
893
+ const { chainId, receipts } = options
894
+ if (receipts.length === 0) return []
895
+ const tidx = c.get('getTidx')(chainId)
896
+ const blocks = [...new Set(receipts.map((receipt) => receipt.blockNumber))]
897
+ const hashes = [...new Set(receipts.map((receipt) => receipt.transactionHash))]
898
+
899
+ // `txs` is sorted by `(block_num, idx)` with no hash index, so the hash list
900
+ // must be paired with the page's block numbers — a bare `hash IN` full-scans
901
+ // the table. Raw-table query, so the inline SQL is cast to `string`.
902
+ const result = await Timing.time(c, 'transactions', () =>
903
+ tidx.fetch({
904
+ chainId,
905
+ query:
906
+ `SELECT ${transactionColumns} FROM txs WHERE block_num IN (${blocks.join(', ')}) AND hash IN (${hashes.map((hash) => `'${hash}'`).join(', ')})` as string,
907
+ }),
908
+ )
909
+
910
+ const byHash = new Map<string, z.output<typeof schema.Transaction>>()
911
+ for (const row of result.rows) {
912
+ const rpc = schema.Rpc.Transaction.safeParse(toRpcTransaction(row))
913
+ if (rpc.success) byHash.set(rpc.data.hash.toLowerCase(), humanizeTransaction(rpc.data))
914
+ }
915
+ return receipts
916
+ .map((receipt) => byHash.get(receipt.transactionHash.toLowerCase()))
917
+ .filter((transaction) => transaction !== undefined)
918
+ }
919
+
920
+ /**
921
+ * Resolves fee-token metadata for each row and embeds it under `meta.feeToken`,
922
+ * sharing a single verified-token fetch. Best-effort: a row whose fee token
923
+ * can't be resolved (or that has none) is returned unchanged. Shared by the
924
+ * transaction and receipt list endpoints (and their address-scoped variants),
925
+ * whose rows all carry an optional `feeToken` address and a `meta` object.
926
+ */
927
+ export async function embedFeeTokens<
928
+ row extends { feeToken?: Hex.Hex | undefined; meta: Record<string, unknown> },
929
+ >(c: Context<App.Environment>, rows: row[], chainId: z.output<typeof Schema.ChainId>) {
930
+ if (rows.length === 0) return rows
931
+ const uniqueTokens = Array.from(
932
+ new Set(
933
+ rows.map((row) => row.feeToken).filter((address): address is Hex.Hex => address != null),
934
+ ),
935
+ )
936
+ if (uniqueTokens.length === 0) return rows
937
+
938
+ const snapshot = await VerifiedTokens.snapshot(c, chainId)
939
+ const resolved = await Timing.time(c, 'tokens', () =>
940
+ Promise.all(
941
+ uniqueTokens.map((token) =>
942
+ Tokens.resolveToken(c, {
943
+ address: token,
944
+ chainId,
945
+ snapshot,
946
+ })
947
+ .then((metadata) => [token, metadata] as const)
948
+ .catch(() => [token, undefined] as const),
949
+ ),
950
+ ),
951
+ )
952
+ const tokensByAddress = new Map(resolved)
953
+ return rows.map((row) => {
954
+ const token = row.feeToken == null ? undefined : tokensByAddress.get(row.feeToken)
955
+ // The runtime shape stays `row`; the generic object spread just can't prove
956
+ // the override keeps it assignable to `row`.
957
+ return (token ? { ...row, meta: { ...row.meta, feeToken: token } } : row) as row
958
+ })
959
+ }
960
+
961
+ export function getTransactions(c: Context<App.Environment>, options: getTransactions.Options) {
962
+ const { chainId } = options
963
+ const store = c.get('store')
964
+ const tidx = c.get('getTidx')(chainId)
965
+ const cursor = options.cursor ? Cursor.decode(options.cursor, ['int', 'int']) : undefined
966
+
967
+ return Timing.time(c, 'transactions', () =>
968
+ Store.memoize(
969
+ async () => {
970
+ const page = await scanTransactions({ tidx }, options)
971
+ return {
972
+ data: page.items.map((item) => item.data),
973
+ nextCursor: page.nextCursor,
974
+ }
975
+ },
976
+ {
977
+ key: `transactions:v1:${chainId}:${options.order}:${options.address ?? ''}:${options.sender ?? ''}:${options.recipient ?? ''}:${options.feePayer ?? ''}:${options.feeToken ?? ''}:${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}`,
978
+ store,
979
+ ttl: Ttl.seconds(15),
980
+ },
981
+ ),
982
+ )
983
+ }
984
+
985
+ /**
986
+ * Context-free core of {@link getTransactions}: builds the `txs` query (with the
987
+ * address-scoped `UNION` planner workaround), applies keyset pagination on
988
+ * `(block_num, idx)`, and reconstructs each row through the same
989
+ * `toRpcTransaction` → `humanizeTransaction` path the detail endpoint uses. Used
990
+ * by the Hono routes (via {@link getTransactions}, which adds Server-Timing +
991
+ * memoization) and by the webhook poller (uncached, ascending, per-row cursors).
992
+ */
993
+ export async function scanTransactions(
994
+ deps: scanTransactions.Deps,
995
+ options: getTransactions.Options,
996
+ ): Promise<scanTransactions.Page> {
997
+ const {
998
+ address,
999
+ chainId,
1000
+ feePayer,
1001
+ feeToken,
1002
+ fromBlock,
1003
+ fromTimestamp,
1004
+ includeCallRecipients,
1005
+ limit,
1006
+ order,
1007
+ recipient,
1008
+ sender,
1009
+ toBlock,
1010
+ toTimestamp,
1011
+ } = options
1012
+ const { tidx } = deps
1013
+ const sortDirection = order === 'asc' ? 'ASC' : 'DESC'
1014
+ // Keyset pagination: anchor the page below the previous row's `(block, idx)`
1015
+ // position instead of a numeric offset, so rows arriving at the head can't
1016
+ // shift items across pages. A malformed cursor decodes to `undefined` and
1017
+ // falls back to the head page.
1018
+ const cursor = options.cursor ? Cursor.decode(options.cursor, ['int', 'int']) : undefined
1019
+ // Bounded positional lane (exclusive with `cursor` at the schema): page 1 is
1020
+ // the head page, so it shares the head's SQL and cache entries; deeper pages
1021
+ // translate to a SQL OFFSET of `(page - 1) * limit` rows.
1022
+ const offset =
1023
+ options.page !== undefined && options.page > 1 ? (options.page - 1) * limit : undefined
1024
+
1025
+ // Raw transactions from the `txs` table (Postgres engine). `txs` is a
1026
+ // standard table, not a signature-decoded CTE, so the inline query is
1027
+ // cast to `string` and needs no `signatures`. Order by block then
1028
+ // in-block index so transactions stay stable across pages, and fetch one
1029
+ // extra row to detect `hasMore` without a separate count query.
1030
+ //
1031
+ // Build the shared filters once (everything except the address-scope
1032
+ // predicate); the `address` filter is special-cased below because the
1033
+ // TIDX engine errors out on `(from = X OR to = X) ORDER BY ...` for
1034
+ // contracts with non-trivial traffic and an index-friendly `UNION`
1035
+ // over the two sides plans cleanly.
1036
+ const filters: string[] = []
1037
+ if (sender !== undefined) filters.push(`"from" = '${sender}'`)
1038
+ if (recipient !== undefined)
1039
+ // For Tempo AA transactions the meaningful recipient is often an inner call
1040
+ // target, so `includeCallRecipients` also matches `calls[].to` (a JSONB array
1041
+ // of `{ to, value, input }`). The `@>` containment hits the partial GIN index
1042
+ // on `calls`; the root `"to"` branch covers non-AA and single-call AA txs
1043
+ // (whose `"to"` mirrors `calls[0].to` and which the partial index omits).
1044
+ filters.push(
1045
+ includeCallRecipients
1046
+ ? `("to" = '${recipient}' OR calls @> '[{"to": "${recipient}"}]')`
1047
+ : `"to" = '${recipient}'`,
1048
+ )
1049
+ if (feePayer !== undefined) filters.push(`fee_payer = '${feePayer}'`)
1050
+ if (feeToken !== undefined) filters.push(`fee_token = '${feeToken}'`)
1051
+ if (fromBlock !== undefined) filters.push(`block_num >= ${fromBlock}`)
1052
+ if (toBlock !== undefined) filters.push(`block_num <= ${toBlock}`)
1053
+ if (fromTimestamp !== undefined) filters.push(`block_timestamp >= '${fromTimestamp}'`)
1054
+ if (toTimestamp !== undefined) filters.push(`block_timestamp <= '${toTimestamp}'`)
1055
+ if (cursor !== undefined)
1056
+ filters.push(
1057
+ Cursor.keyset([
1058
+ { literal: Cursor.literal(cursor[0]!, 'int'), name: 'block_num', order },
1059
+ { literal: Cursor.literal(cursor[1]!, 'int'), name: 'idx', order },
1060
+ ]),
1061
+ )
1062
+
1063
+ // For address-scoped queries, run one branch per side and `UNION` so
1064
+ // each branch can use its own (`from`/`to`) index. The `ORDER BY` and
1065
+ // `LIMIT` must live *inside* each branch — TIDX's planner crashes on
1066
+ // a UNION whose outer query orders by `block_num, idx` for
1067
+ // high-traffic contracts (and on the `(from = X OR to = X) ORDER BY`
1068
+ // form). Pushing the sort+limit down means each branch returns at
1069
+ // most `limit + 1` index-ordered rows, the outer query re-merges,
1070
+ // and plain `UNION` dedupes the self-transfer case where
1071
+ // `from = to = address`.
1072
+ // In the offset lane each branch must over-fetch the full window
1073
+ // (`offset + limit + 1` rows) because the skip applies to the merged set:
1074
+ // the outer query re-sorts the union and discards the first `offset` rows.
1075
+ const sideQuery = (side: 'from' | 'to', addr: string) => {
1076
+ const where = [`"${side}" = '${addr}'`, ...filters].join(' AND ')
1077
+ return `(SELECT ${transactionColumns} FROM txs WHERE ${where} ORDER BY block_num ${sortDirection}, idx ${sortDirection} LIMIT ${(offset ?? 0) + limit + 1})`
1078
+ }
1079
+ const skip = offset !== undefined ? ` OFFSET ${offset}` : ''
1080
+ const query =
1081
+ address !== undefined
1082
+ ? `
1083
+ ${sideQuery('from', address)}
1084
+ UNION
1085
+ ${sideQuery('to', address)}
1086
+ ORDER BY block_num ${sortDirection}, idx ${sortDirection}
1087
+ LIMIT ${limit + 1}${skip}
1088
+ `
1089
+ : `
1090
+ SELECT ${transactionColumns}
1091
+ FROM txs
1092
+ ${filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''}
1093
+ ORDER BY block_num ${sortDirection}, idx ${sortDirection}
1094
+ LIMIT ${limit + 1}${skip}
1095
+ `
1096
+
1097
+ const result = await tidx.fetch({ chainId, query: query as string })
1098
+
1099
+ // The next page anchors below the last fetched row's `(block, idx)`.
1100
+ const page = Cursor.paginate({
1101
+ rows: result.rows,
1102
+ limit,
1103
+ key: (row) => {
1104
+ const block = Value.toNumber(row['block_num'])
1105
+ const index = Value.toNumber(row['idx'])
1106
+ return block !== undefined && index !== undefined ? [block, index] : undefined
1107
+ },
1108
+ })
1109
+
1110
+ // Reconstruct the RPC shape from the indexed columns, then humanize it
1111
+ // through the same path as the detail endpoint so the list and detail
1112
+ // responses are identical. Rows that fail to reconstruct are skipped.
1113
+ const items: scanTransactions.Item[] = []
1114
+ for (const row of page.rows) {
1115
+ const rpc = schema.Rpc.Transaction.safeParse(toRpcTransaction(row))
1116
+ const block = Value.toNumber(row['block_num'])
1117
+ const index = Value.toNumber(row['idx'])
1118
+ if (rpc.success && block !== undefined && index !== undefined)
1119
+ items.push({ cursor: [block, index], data: humanizeTransaction(rpc.data) })
1120
+ }
1121
+
1122
+ return { hasMore: page.hasMore, items, limit, nextCursor: page.nextCursor }
1123
+ }
1124
+
1125
+ export declare namespace getTransactions {
1126
+ type Options = {
1127
+ address?: z.output<typeof Schema.Address> | undefined
1128
+ chainId: z.output<typeof Schema.ChainId>
1129
+ /** Opaque keyset cursor anchoring the page; omit for the head page. */
1130
+ cursor?: string | undefined
1131
+ feePayer?: z.output<typeof Schema.Address> | undefined
1132
+ feeToken?: z.output<typeof Schema.Address> | undefined
1133
+ fromBlock?: number | undefined
1134
+ fromTimestamp?: string | undefined
1135
+ /**
1136
+ * When true, the `recipient` filter also matches any inner account-abstraction
1137
+ * call target (`calls[].to`), not just the root `to`. Off by default so read
1138
+ * endpoints keep root-level recipient semantics.
1139
+ */
1140
+ includeCallRecipients?: boolean | undefined
1141
+ limit: number
1142
+ /** 1-indexed page number (positional pagination; exclusive with `cursor`). */
1143
+ page?: number | undefined
1144
+ order: 'asc' | 'desc'
1145
+ recipient?: z.output<typeof Schema.Address> | undefined
1146
+ sender?: z.output<typeof Schema.Address> | undefined
1147
+ /**
1148
+ * Filter to transactions whose receipt shows this execution status.
1149
+ * Handled by {@link listTransactions} via the receipts-driven path; the raw
1150
+ * `txs` scan ignores it (`txs` does not store execution status).
1151
+ */
1152
+ status?: 'reverted' | 'success' | undefined
1153
+ toBlock?: number | undefined
1154
+ toTimestamp?: string | undefined
1155
+ }
1156
+ }
1157
+
1158
+ /**
1159
+ * Builds the non-cursor, non-address-scope `txs` filters shared by the page
1160
+ * ({@link scanTransactions}) and the capped count ({@link countTransactions}),
1161
+ * so the count always matches the page it annotates. The address scope is
1162
+ * handled separately (it `UNION`s the two sides, as the page does).
1163
+ */
1164
+ function countFilters(options: countTransactions.Options): string[] {
1165
+ const filters: string[] = []
1166
+ if (options.sender !== undefined) filters.push(`"from" = '${options.sender}'`)
1167
+ if (options.recipient !== undefined) filters.push(`"to" = '${options.recipient}'`)
1168
+ if (options.feePayer !== undefined) filters.push(`fee_payer = '${options.feePayer}'`)
1169
+ if (options.feeToken !== undefined) filters.push(`fee_token = '${options.feeToken}'`)
1170
+ if (options.fromBlock !== undefined) filters.push(`block_num >= ${options.fromBlock}`)
1171
+ if (options.toBlock !== undefined) filters.push(`block_num <= ${options.toBlock}`)
1172
+ if (options.fromTimestamp !== undefined)
1173
+ filters.push(`block_timestamp >= '${options.fromTimestamp}'`)
1174
+ if (options.toTimestamp !== undefined) filters.push(`block_timestamp <= '${options.toTimestamp}'`)
1175
+ return filters
1176
+ }
1177
+
1178
+ /**
1179
+ * Capped total count of transactions matching the same filters as the page.
1180
+ *
1181
+ * A `status` filter is delegated to {@link Receipts.countReceipts}, mirroring
1182
+ * the page's receipts-driven `status` path (execution status lives on receipts,
1183
+ * not `txs`). The address scope reuses the page's `UNION` of the two sides —
1184
+ * each side hits its own (`from`/`to`) index, and the deduped keyset count
1185
+ * avoids double-counting self-transactions (`from = to`). `totalCountCapped: true` is a
1186
+ * lower bound — at least {@link Schema.countCap} matches.
1187
+ */
1188
+ export function countTransactions(
1189
+ c: Context<App.Environment>,
1190
+ options: countTransactions.Options,
1191
+ ): Promise<z.output<typeof Schema.CountMeta>> {
1192
+ // Status lives on receipts, so route the count through the same path the page
1193
+ // uses: count receipts under the identical filters.
1194
+ if (options.status !== undefined) return Receipts.countReceipts(c, options)
1195
+
1196
+ const { address, chainId } = options
1197
+ const store = c.get('store')
1198
+ const tidx = c.get('getTidx')(chainId)
1199
+ return Timing.time(c, 'transactions_count', () =>
1200
+ Store.memoize(
1201
+ async () => {
1202
+ const filters = countFilters(options)
1203
+ let query: string
1204
+ if (address !== undefined) {
1205
+ const fromWhere = [`"from" = '${address}'`, ...filters].join(' AND ')
1206
+ const toWhere = [`"to" = '${address}'`, ...filters].join(' AND ')
1207
+ query = `
1208
+ SELECT count(*) AS total
1209
+ FROM (
1210
+ (SELECT block_num, idx FROM txs WHERE ${fromWhere} LIMIT ${Schema.countCap})
1211
+ UNION
1212
+ (SELECT block_num, idx FROM txs WHERE ${toWhere} LIMIT ${Schema.countCap})
1213
+ ) AS capped
1214
+ `
1215
+ } else {
1216
+ const where = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''
1217
+ query = `
1218
+ SELECT count(*) AS total
1219
+ FROM (
1220
+ SELECT 1 FROM txs ${where} LIMIT ${Schema.countCap}
1221
+ ) AS capped
1222
+ `
1223
+ }
1224
+ const result = await tidx.fetch({ chainId, query: query as string })
1225
+ const totalCount = Value.toNumber(result.rows[0]?.['total']) ?? 0
1226
+ return { totalCountCapped: totalCount >= Schema.countCap, totalCount }
1227
+ },
1228
+ {
1229
+ key: `transactions-count:v1:${chainId}:${address ?? ''}:${options.sender ?? ''}:${options.recipient ?? ''}:${options.feePayer ?? ''}:${options.feeToken ?? ''}:${options.fromBlock ?? ''}:${options.toBlock ?? ''}:${options.fromTimestamp ?? ''}:${options.toTimestamp ?? ''}`,
1230
+ store,
1231
+ ttl: Ttl.seconds(15),
1232
+ },
1233
+ ),
1234
+ )
1235
+ }
1236
+
1237
+ export declare namespace countTransactions {
1238
+ /** Filters for {@link countTransactions}: the page filters minus pagination. */
1239
+ type Options = Pick<
1240
+ getTransactions.Options,
1241
+ | 'address'
1242
+ | 'chainId'
1243
+ | 'feePayer'
1244
+ | 'feeToken'
1245
+ | 'fromBlock'
1246
+ | 'fromTimestamp'
1247
+ | 'recipient'
1248
+ | 'sender'
1249
+ | 'status'
1250
+ | 'toBlock'
1251
+ | 'toTimestamp'
1252
+ >
1253
+ }
1254
+
1255
+ export declare namespace scanTransactions {
1256
+ /** Dependencies for {@link scanTransactions}. */
1257
+ type Deps = {
1258
+ /** TIDX query client for the target chain. */
1259
+ tidx: Tidx.Client
1260
+ }
1261
+ /** A humanized transaction row plus its `(block_num, idx)` cursor. */
1262
+ type Item = {
1263
+ /** Keyset position of the row. */
1264
+ cursor: readonly [blockNumber: number, transactionIndex: number]
1265
+ /** The humanized transaction (same shape the read endpoint returns). */
1266
+ data: z.output<typeof schema.Transaction>
1267
+ }
1268
+ /** A page of scanned transactions. */
1269
+ type Page = {
1270
+ hasMore: boolean
1271
+ items: readonly Item[]
1272
+ limit: number
1273
+ nextCursor: string | null
1274
+ }
1275
+ }
1276
+
1277
+ /**
1278
+ * The `txs` columns {@link toRpcTransaction} reconstructs a transaction from.
1279
+ * Shared by the list scan and the receipt-driven `status` path so the two
1280
+ * queries cannot drift from the reconstruction.
1281
+ */
1282
+ const transactionColumns = `block_num, block_timestamp, idx, hash, type, "from", "to", value, input,
1283
+ gas_limit, max_fee_per_gas, max_priority_fee_per_gas,
1284
+ nonce_key, nonce, fee_token, fee_payer, valid_before, valid_after`
1285
+
1286
+ /**
1287
+ * Re-encodes a TIDX `txs` row into the JSON-RPC transaction shape, which becomes
1288
+ * the `rpc` payload that the list endpoint humanizes through the same path as
1289
+ * the detail endpoint. Numeric columns become `0x` quantities; fields the
1290
+ * indexer does not store (signatures, access list) are omitted, and `blockHash`
1291
+ * is null since the schema requires it. The all-zero `nonce_key` that the
1292
+ * indexer records for non-Tempo transactions is dropped to match the RPC
1293
+ * response, which omits it.
1294
+ */
1295
+ function toRpcTransaction(row: Record<string, unknown>) {
1296
+ const timestamp = Value.toIsoDateTime(row['block_timestamp'])
1297
+ const nonceKey = row['nonce_key']
1298
+ return {
1299
+ blockHash: null,
1300
+ blockNumber: Value.toHex(row['block_num']) ?? null,
1301
+ blockTimestamp: timestamp ? Value.toHex(Math.floor(Date.parse(timestamp) / 1000)) : undefined,
1302
+ feeToken: typeof row['fee_token'] === 'string' ? row['fee_token'] : undefined,
1303
+ from: row['from'],
1304
+ gas: Value.toHex(row['gas_limit']),
1305
+ hash: row['hash'],
1306
+ input: typeof row['input'] === 'string' ? row['input'] : undefined,
1307
+ maxFeePerGas: Value.toHex(row['max_fee_per_gas']),
1308
+ maxPriorityFeePerGas: Value.toHex(row['max_priority_fee_per_gas']),
1309
+ nonce: Value.toHex(row['nonce']),
1310
+ nonceKey: typeof nonceKey === 'string' && !/^0x0+$/.test(nonceKey) ? nonceKey : undefined,
1311
+ to: typeof row['to'] === 'string' ? row['to'] : undefined,
1312
+ transactionIndex: Value.toHex(row['idx']) ?? null,
1313
+ type: Value.toHex(row['type']),
1314
+ validAfter: Value.toHex(row['valid_after']),
1315
+ validBefore: Value.toHex(row['valid_before']),
1316
+ value: Value.toHex(row['value']),
1317
+ }
1318
+ }
1319
+
1320
+ /** Humanizes one call within a Tempo account-abstraction transaction. */
1321
+ function humanizeCall(call: z.output<typeof schema.Call>): z.output<typeof schema.HumanCall> {
1322
+ return {
1323
+ data: call.input ?? call.data ?? undefined,
1324
+ to: call.to ?? null,
1325
+ }
1326
+ }
1327
+
1328
+ /**
1329
+ * Humanizes a JSON-RPC transaction: decodes `type`, converts hex quantities to
1330
+ * decimal strings, hex block fields to numbers, and timestamps to ISO 8601,
1331
+ * while preserving the verbatim payload under `rpc`. The same function serves
1332
+ * the detail endpoint (RPC source) and the list endpoint (reconstructed from the
1333
+ * indexer), so both expose an identical shape.
1334
+ */
1335
+ export function humanizeTransaction(
1336
+ rpc: z.output<typeof schema.Rpc.Transaction>,
1337
+ ): z.output<typeof schema.Transaction> {
1338
+ return {
1339
+ blockHash: rpc.blockHash,
1340
+ blockNumber: Value.hexToNumber(rpc.blockNumber) ?? null,
1341
+ calls: rpc.calls?.map(humanizeCall),
1342
+ chainId: Value.hexToNumber(rpc.chainId),
1343
+ feeToken: rpc.feeToken ?? undefined,
1344
+ gas: Value.hexToNumber(rpc.gas) ?? 0,
1345
+ gasPrice: Value.fromHex(rpc.gasPrice),
1346
+ hash: rpc.hash,
1347
+ id: rpc.hash,
1348
+ input: rpc.input,
1349
+ maxFeePerGas: Value.fromHex(rpc.maxFeePerGas),
1350
+ maxPriorityFeePerGas: Value.fromHex(rpc.maxPriorityFeePerGas),
1351
+ meta: { rpc },
1352
+ nonce: Value.hexToNumber(rpc.nonce) ?? 0,
1353
+ nonceKey: rpc.nonceKey,
1354
+ recipient: rpc.to ?? null,
1355
+ sender: rpc.from,
1356
+ timestamp: Value.hexSecondsToIso(rpc.blockTimestamp) ?? null,
1357
+ transactionIndex: Value.hexToNumber(rpc.transactionIndex) ?? null,
1358
+ type: decodeTransactionType(rpc.type),
1359
+ validAfter: rpc.validAfter == null ? undefined : Value.hexSecondsToIso(rpc.validAfter),
1360
+ validBefore: rpc.validBefore == null ? undefined : Value.hexSecondsToIso(rpc.validBefore),
1361
+ value: Value.fromHex(rpc.value) ?? '0',
1362
+ }
1363
+ }