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,1193 @@
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 * as Timing from '../internal/Timing.js'
14
+ import * as Ttl from '../internal/Ttl.js'
15
+ import * as Value from '../internal/Value.js'
16
+ import * as Tokens from './tokens.js'
17
+
18
+ // Receipts and transactions deliberately form an import cycle (receipts reuses
19
+ // the transaction-type decoder and fee-token embedder; transactions embeds
20
+ // receipts). Neither module may touch the other's exports during module
21
+ // evaluation: schema references across the cycle must stay inside `z.lazy`, and
22
+ // function references are call-time only.
23
+ import { decodeTransactionType, embedFeeTokens, schema as txSchema } from './transactions.js'
24
+
25
+ /** Example transaction hash surfaced in OpenAPI docs. */
26
+ const exampleHash = '0x515801d7f9a5ac705e793e85904c9c69b3f1694b465cc8fb6ba3f0298dc82665'
27
+
28
+ // Tempo gas prices are denominated in attodollars (10^-18 USD) while fee-token
29
+ // balances use 6 decimals (microdollars, 10^-6 USD), so the fee charged to the
30
+ // fee payer — in the fee token's base units — is
31
+ // `ceil(gasUsed * effectiveGasPrice / 10^12)` (ceiling division). Mirrors
32
+ // `calc_gas_balance_spending` in tempoxyz/tempo.
33
+ const feeScalingFactor = 10n ** 12n
34
+
35
+ /**
36
+ * Computes the fee charged to the fee payer, in the fee token's base units, from
37
+ * the receipt's `gasUsed` and decimal `effectiveGasPrice`. Returns undefined
38
+ * when either input is missing.
39
+ */
40
+ function computeFeeAmount(gasUsed: number | undefined, effectiveGasPrice: string | undefined) {
41
+ if (gasUsed === undefined || effectiveGasPrice === undefined) return undefined
42
+ const product = BigInt(gasUsed) * BigInt(effectiveGasPrice)
43
+ return ((product + feeScalingFactor - 1n) / feeScalingFactor).toString()
44
+ }
45
+
46
+ /** Zod schemas owned by the receipt handlers. */
47
+ export namespace schema {
48
+ // TODO(ox/zod): replace with the zod schema for `ox`'s `Log.Rpc` once ox ships
49
+ // a `zod` entrypoint.
50
+ /** One event log emitted by a transaction (RPC format). */
51
+ export const Log = z
52
+ .looseObject({
53
+ address: Schema.Address.check(
54
+ z.describe('The contract address that emitted this event log.'),
55
+ ),
56
+ blockHash: z
57
+ .nullable(Schema.Hash)
58
+ .check(z.describe('The block hash once included, or `null` while pending.')),
59
+ blockNumber: z
60
+ .nullable(Schema.Quantity)
61
+ .check(z.describe('The block number once included, or `null` while pending.')),
62
+ blockTimestamp: z
63
+ .nullish(Schema.Quantity)
64
+ .check(z.describe('Tempo-provided block timestamp for this log.')),
65
+ data: Schema.Hex.check(
66
+ z.describe('ABI-encoded data for the event parameters that are not indexed.'),
67
+ z.meta({ examples: ['0xdeadbeef'] }),
68
+ ),
69
+ logIndex: z
70
+ .nullable(Schema.Quantity)
71
+ .check(z.describe('The log position within the block, or `null` while pending.')),
72
+ removed: z
73
+ .boolean()
74
+ .check(
75
+ z.describe(
76
+ 'Whether this log was removed by a chain reorganization; Tempo finality means finalized logs do not reorg.',
77
+ ),
78
+ z.meta({ examples: [false] }),
79
+ ),
80
+ topics: z
81
+ .array(Schema.Hash)
82
+ .check(
83
+ z.describe(
84
+ 'Indexed event topics, including the event signature hash as the first topic.',
85
+ ),
86
+ ),
87
+ transactionHash: z
88
+ .nullable(Schema.Hash)
89
+ .check(z.describe('The transaction hash for this log, or `null` while pending.')),
90
+ transactionIndex: z
91
+ .nullable(Schema.Quantity)
92
+ .check(z.describe('The transaction position within the block, or `null` while pending.')),
93
+ })
94
+ .check(z.describe('One event log emitted by a contract during transaction execution.'))
95
+
96
+ /** The verbatim JSON-RPC payloads, surfaced under each resource's `rpc` field. */
97
+ export namespace Rpc {
98
+ // TODO(ox/zod): replace with the zod schema for `ox/tempo`'s
99
+ // `TransactionReceipt.Rpc` once ox ships a `zod` entrypoint. `blockHash`,
100
+ // `logsBloom`, and `blockTimestamp` are nullish/optional so the receipt
101
+ // *list* (reconstructed from the indexer, which stores neither block hash nor
102
+ // bloom but does carry the block timestamp) reuses this same shape; the
103
+ // detail endpoint's verbatim RPC receipt populates all of them.
104
+ /** A JSON-RPC transaction receipt, passed through verbatim. */
105
+ export const Receipt = z
106
+ .looseObject({
107
+ blobGasPrice: z
108
+ .optional(Schema.Quantity)
109
+ .check(z.describe('Blob gas price for EIP-4844 blob data.')),
110
+ blobGasUsed: z
111
+ .optional(Schema.Quantity)
112
+ .check(z.describe('Blob gas used by EIP-4844 blob data.')),
113
+ blockHash: z
114
+ .nullable(Schema.Hash)
115
+ .check(
116
+ z.describe('The block hash, or `null` for receipts reconstructed from the index.'),
117
+ ),
118
+ blockNumber: Schema.Quantity.check(
119
+ z.describe('The block number that included this receipt.'),
120
+ ),
121
+ blockTimestamp: z
122
+ .nullish(Schema.Quantity)
123
+ .check(
124
+ z.describe(
125
+ 'Block timestamp added when this receipt is reconstructed from indexed data.',
126
+ ),
127
+ ),
128
+ contractAddress: z
129
+ .nullish(Schema.Address)
130
+ .check(
131
+ z.describe(
132
+ 'The created contract address, or `null` if the transaction did not create a contract.',
133
+ ),
134
+ ),
135
+ cumulativeGasUsed: Schema.Quantity.check(
136
+ z.describe('Total gas used in the block up to and including this transaction.'),
137
+ ),
138
+ effectiveGasPrice: Schema.Quantity.check(
139
+ z.describe('Effective gas price paid for this transaction.'),
140
+ ),
141
+ feePayer: z
142
+ .nullish(Schema.Address)
143
+ .check(z.describe('The address that paid the transaction fee.')),
144
+ feeToken: z
145
+ .nullish(Schema.Address)
146
+ .check(
147
+ z.describe(
148
+ 'The token used to pay the fee; on Tempo this is a USD stablecoin such as `pathUSD`.',
149
+ ),
150
+ ),
151
+ from: Schema.Address.check(
152
+ z.describe('The address that sent or authorized the transaction.'),
153
+ ),
154
+ gasUsed: Schema.Quantity.check(z.describe('Gas actually used to execute the transaction.')),
155
+ logs: z
156
+ .array(Log)
157
+ .check(z.describe('Event logs emitted by contracts while this transaction executed.')),
158
+ logsBloom: z
159
+ .optional(Schema.Hex)
160
+ .check(
161
+ z.describe('Bloom filter summarizing the logs in this receipt.'),
162
+ z.meta({ examples: ['0x00'] }),
163
+ ),
164
+ root: z.optional(Schema.Hex).check(
165
+ z.describe('Post-transaction state root used by pre-Byzantium Ethereum receipts.'),
166
+ z.meta({
167
+ examples: ['0x515801d7f9a5ac705e793e85904c9c69b3f1694b465cc8fb6ba3f0298dc82665'],
168
+ }),
169
+ ),
170
+ status: Schema.Quantity.check(
171
+ z.describe(
172
+ 'Execution status as a hex quantity: `0x1` means success and `0x0` means reverted.',
173
+ ),
174
+ z.meta({ examples: ['0x1'] }),
175
+ ),
176
+ to: z
177
+ .nullable(Schema.Address)
178
+ .check(
179
+ z.describe('The recipient address, or `null` when the transaction created a contract.'),
180
+ ),
181
+ transactionHash: Schema.Hash.check(
182
+ z.describe(
183
+ 'A 32-byte hash, `0x`-prefixed and returned in lowercase, that identifies the transaction.',
184
+ ),
185
+ ),
186
+ transactionIndex: Schema.Quantity.check(
187
+ z.describe('The transaction position within its block.'),
188
+ ),
189
+ type: Schema.Hex.check(
190
+ z.describe('Raw transaction type byte, such as `0x2` for EIP-1559 or `0x76` for Tempo.'),
191
+ z.meta({ examples: ['0x2'] }),
192
+ ),
193
+ })
194
+ .check(z.describe('The transaction receipt exactly as returned by JSON-RPC.'))
195
+ }
196
+
197
+ /**
198
+ * Metadata for a receipt: the verbatim JSON-RPC receipt (always present) plus
199
+ * resources embedded on demand via `include`.
200
+ */
201
+ export const ReceiptMeta = z
202
+ .object({
203
+ feeToken: z
204
+ .optional(z.lazy(() => Tokens.schema.getToken.Response))
205
+ .check(z.describe('Fee-token details included only when you request `include=feeToken`.')),
206
+ rpc: Rpc.Receipt.check(z.describe('The original JSON-RPC receipt payload.')),
207
+ })
208
+ .check(
209
+ z.describe(
210
+ 'Metadata for this receipt: the original JSON-RPC payload plus any requested `include` resources.',
211
+ ),
212
+ )
213
+
214
+ /**
215
+ * A humanized transaction receipt: decoded `status`/`type`, plain-number gas
216
+ * and block fields, and decimal-string fee/price values. The raw event logs
217
+ * are surfaced verbatim under `logs`, and the full JSON-RPC receipt under
218
+ * `meta.rpc`.
219
+ */
220
+ export const Receipt = z
221
+ .object({
222
+ blockHash: z
223
+ .nullable(Schema.Hash)
224
+ .check(
225
+ z.describe(
226
+ 'The block hash, or `null` when the receipt was reconstructed from indexed data.',
227
+ ),
228
+ ),
229
+ blockNumber: z
230
+ .number()
231
+ .check(
232
+ z.int(),
233
+ z.nonnegative(),
234
+ z.describe('The block number that included this receipt.'),
235
+ z.meta({ examples: [23456789] }),
236
+ ),
237
+ contractAddress: z
238
+ .nullable(Schema.Address)
239
+ .check(
240
+ z.describe(
241
+ 'The created contract address, or `null` if the transaction did not create a contract.',
242
+ ),
243
+ ),
244
+ cumulativeGasUsed: z
245
+ .number()
246
+ .check(
247
+ z.int(),
248
+ z.nonnegative(),
249
+ z.describe('Total gas used in the block up to and including this transaction.'),
250
+ z.meta({ examples: [21000] }),
251
+ ),
252
+ effectiveGasPrice: Schema.DecimalString.check(
253
+ z.describe('Effective gas price paid for this transaction, in attodollars per gas.'),
254
+ ),
255
+ feeAmount: Schema.DecimalString.check(
256
+ z.describe(
257
+ 'Fee charged to the fee payer, as a decimal integer string in the fee token’s base units.',
258
+ ),
259
+ ),
260
+ feePayer: z
261
+ .optional(Schema.Address)
262
+ .check(z.describe('The address that paid the transaction fee.')),
263
+ feeToken: z
264
+ .optional(Schema.Address)
265
+ .check(
266
+ z.describe(
267
+ 'The token used to pay the fee; on Tempo this is a USD stablecoin such as `pathUSD`.',
268
+ ),
269
+ ),
270
+ gasUsed: z
271
+ .number()
272
+ .check(
273
+ z.int(),
274
+ z.nonnegative(),
275
+ z.describe('Gas actually used to execute the transaction.'),
276
+ z.meta({ examples: [21000] }),
277
+ ),
278
+ id: Schema.Hash.check(
279
+ z.describe('Stable resource ID for this API response; it is the transaction hash.'),
280
+ ),
281
+ logs: z
282
+ .array(Log)
283
+ .check(
284
+ z.describe('Event logs emitted by this transaction, returned in JSON-RPC log format.'),
285
+ ),
286
+ meta: ReceiptMeta.check(
287
+ z.describe('The original JSON-RPC payload plus any resources requested with `include`.'),
288
+ ),
289
+ recipient: z
290
+ .nullable(Schema.Address)
291
+ .check(
292
+ z.describe('The recipient address, or `null` when the transaction created a contract.'),
293
+ ),
294
+ sender: Schema.Address.check(
295
+ z.describe('The address that sent or authorized the transaction.'),
296
+ ),
297
+ status: z
298
+ .enum(['success', 'reverted'])
299
+ .check(
300
+ z.describe('Whether the transaction succeeded or reverted.'),
301
+ z.meta({ examples: ['success'] }),
302
+ ),
303
+ timestamp: z
304
+ .nullable(z.iso.datetime())
305
+ .check(
306
+ z.describe('The block timestamp as ISO 8601, or `null` when it is unavailable.'),
307
+ z.meta({ examples: ['2024-01-01T00:00:00Z'] }),
308
+ ),
309
+ transactionHash: Schema.Hash.check(
310
+ z.describe(
311
+ 'A 32-byte hash, `0x`-prefixed and returned in lowercase, that identifies the transaction.',
312
+ ),
313
+ ),
314
+ transactionIndex: z
315
+ .number()
316
+ .check(
317
+ z.int(),
318
+ z.nonnegative(),
319
+ z.describe('The transaction position within its block.'),
320
+ z.meta({ examples: [0] }),
321
+ ),
322
+ type: z.lazy(() => txSchema.TransactionType).check(z.meta({ examples: ['eip1559'] })),
323
+ })
324
+ .check(
325
+ z.describe(
326
+ 'A transaction outcome formatted for API clients, with status, gas, fees, logs, and the original JSON-RPC receipt under `meta.rpc`.',
327
+ ),
328
+ )
329
+
330
+ /** Schemas for the getReceipt operation. */
331
+ export namespace getReceipt {
332
+ /** Path parameters for receipt requests. */
333
+ export const Params = z
334
+ .object({
335
+ transactionHash: Schema.Hash.check(
336
+ z.describe(
337
+ 'A 32-byte hash, `0x`-prefixed and returned in lowercase, that identifies the transaction.',
338
+ ),
339
+ z.meta({ examples: [exampleHash] }),
340
+ ),
341
+ })
342
+ .check(z.describe('Path parameters used to look up a receipt by transaction hash.'))
343
+
344
+ /** Optional related resource that callers opt into via `include`. */
345
+ export const Include = z
346
+ .enum(['feeToken'])
347
+ .check(z.describe('Related resource you can opt into with the `include` query parameter.'))
348
+
349
+ /**
350
+ * Parses a comma-separated `include` query value into a list of optional
351
+ * resources to embed. Extra lookups (e.g. fee-token metadata) only run when
352
+ * explicitly requested, keeping the base receipt response fast.
353
+ */
354
+ export const includeQuery = Schema.includeQuery(
355
+ Include,
356
+ 'Comma-separated related resources to include, such as `feeToken`.',
357
+ )
358
+
359
+ /** Query parameters for receipt requests. */
360
+ export const Query = z
361
+ .strictObject({
362
+ chainId: Schema.ChainIdQuery,
363
+ include: includeQuery,
364
+ })
365
+ .check(z.describe('Query parameters for looking up a receipt.'))
366
+
367
+ /** A humanized receipt, with the verbatim JSON-RPC payload under `meta.rpc`. */
368
+ export const Response = Receipt
369
+ }
370
+
371
+ /** Schemas for the getReceipts (list) operation. */
372
+ export namespace getReceipts {
373
+ /**
374
+ * Optional resources for the receipt list: the per-row `feeToken` embed
375
+ * plus the response-wide capped `totalCount`. The detail route keeps its
376
+ * own `feeToken`-only {@link getReceipt.Include}.
377
+ */
378
+ export const Include = z
379
+ .enum(['feeToken', 'totalCount'])
380
+ .check(
381
+ z.describe('Related resources to include in each receipt row or in response metadata.'),
382
+ )
383
+
384
+ /** Parses the comma-separated `include` value for the receipt list. */
385
+ export const includeQuery = Schema.includeQuery(
386
+ Include,
387
+ 'Comma-separated related resources to include, such as `feeToken,totalCount`.',
388
+ )
389
+
390
+ /** Query parameters for receipt list requests. */
391
+ export const Query = z
392
+ .strictObject({
393
+ address: z
394
+ .optional(Schema.Address)
395
+ .check(
396
+ z.describe(
397
+ 'Filter to receipts whose transaction has this address as sender or recipient.',
398
+ ),
399
+ ),
400
+ 'blockNumber.from': Schema.blockNumberBound('receipts', 'from'),
401
+ 'blockNumber.to': Schema.blockNumberBound('receipts', 'to'),
402
+ chainId: Schema.ChainIdQuery,
403
+ cursor: Schema.Cursor,
404
+ feePayer: z
405
+ .optional(Schema.Address)
406
+ .check(z.describe('Filter to receipts whose transaction fee was paid by this address.')),
407
+ feeToken: z
408
+ .optional(Schema.TokenAddress)
409
+ .check(
410
+ z.describe('Filter to receipts whose transaction fee was paid in this token address.'),
411
+ ),
412
+ include: includeQuery,
413
+ limit: Schema.Limit,
414
+ order: Schema.Order,
415
+ page: Schema.Page,
416
+ recipient: z
417
+ .optional(Schema.Address)
418
+ .check(z.describe('Filter to receipts for transactions sent to this recipient address.')),
419
+ sender: z
420
+ .optional(Schema.Address)
421
+ .check(z.describe('Filter to receipts for transactions sent by this sender address.')),
422
+ status: z
423
+ .optional(z.enum(['success', 'reverted']))
424
+ .check(
425
+ z.describe('Filter to receipts with this execution status.'),
426
+ z.meta({ examples: ['success'] }),
427
+ ),
428
+ 'timestamp.from': Schema.timestampBound('receipts', 'from'),
429
+ 'timestamp.to': Schema.timestampBound('receipts', 'to'),
430
+ })
431
+ .check(...Schema.pageChecks(), z.describe('Query parameters for listing receipts.'))
432
+
433
+ /**
434
+ * Page of receipts. Each item reuses the humanized receipt returned by
435
+ * `GET /transactions/:transactionHash/receipt`, so the list and detail endpoints expose one
436
+ * identical shape. Indexed list rows are reconstructed from the indexer,
437
+ * which stores neither the block hash nor the event logs, so those fields are
438
+ * `null`/empty in the list `meta.rpc` payload.
439
+ */
440
+ export const Response = z
441
+ .object({
442
+ data: z.array(getReceipt.Response).check(z.describe('Receipts on this page.')),
443
+ meta: z
444
+ .optional(Schema.CountMeta)
445
+ .check(
446
+ z.describe('Response-level metadata requested with `include`, such as `totalCount`.'),
447
+ ),
448
+ nextCursor: Schema.NextCursor,
449
+ })
450
+ .check(z.describe('A page of receipts ordered by block number and transaction position.'))
451
+ }
452
+ }
453
+
454
+ /** Creates receipt handlers. */
455
+ export function receipts() {
456
+ return new Hono<App.Environment>()
457
+ .get(
458
+ '/receipts',
459
+ Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }),
460
+ OpenApi.validate('query', schema.getReceipts.Query, {
461
+ code: 'query_invalid',
462
+ message: 'Invalid query parameters',
463
+ }),
464
+ OpenApi.describeRoute({
465
+ description:
466
+ 'List transaction receipts across Tempo, with filters for addresses, blocks, timestamps, fees, and status.',
467
+ operationId: 'getReceipts',
468
+ responses: OpenApi.responses({
469
+ errors: { 502: 'Could not read indexed receipt data from TIDX.' },
470
+ success: {
471
+ description: 'A page of transaction receipts.',
472
+ schema: schema.getReceipts.Response,
473
+ },
474
+ }),
475
+ summary: 'List transaction receipts',
476
+ tags: ['Transactions'],
477
+ }),
478
+ Cache.response<App.Environment>({
479
+ cacheControl: Cache.policies.feed,
480
+ name: 'tempo-api:receipts:v1',
481
+ key: (c) => Cache.urlKey(c, schema.getReceipts.Query),
482
+ }),
483
+ async (c) => {
484
+ if (Auth.narrowAccess) return Auth.paidAccessError(c)
485
+ if (OpenApi.narrowValidation)
486
+ return OpenApi.validationError(c, {
487
+ code: 'query_invalid',
488
+ message: 'Invalid query parameters',
489
+ })
490
+
491
+ const query = c.req.valid('query')
492
+ const chainId = query.chainId ?? c.get('chainId')
493
+
494
+ // Cursor pages are anchored below the head and effectively immutable, so
495
+ // cache them longer; the head page keeps the route's short `feed` default.
496
+ if (query.cursor !== undefined) Cache.setPolicy(c, Cache.policies.immutable)
497
+
498
+ try {
499
+ // `totalCount` is opt-in and shares the page's filters; run it
500
+ // concurrently with the page. Best-effort: a count failure omits
501
+ // `meta` rather than failing the page.
502
+ const countPromise = query.include.includes('totalCount')
503
+ ? countReceipts(c, {
504
+ address: query.address,
505
+ chainId,
506
+ feePayer: query.feePayer,
507
+ feeToken: query.feeToken,
508
+ fromBlock: query['blockNumber.from'],
509
+ fromTimestamp: query['timestamp.from'],
510
+ recipient: query.recipient,
511
+ sender: query.sender,
512
+ status: query.status,
513
+ toBlock: query['blockNumber.to'],
514
+ toTimestamp: query['timestamp.to'],
515
+ }).catch(() => undefined)
516
+ : undefined
517
+
518
+ const receipts = await listReceipts(c, {
519
+ address: query.address,
520
+ chainId,
521
+ cursor: query.cursor,
522
+ feePayer: query.feePayer,
523
+ feeToken: query.feeToken,
524
+ fromBlock: query['blockNumber.from'],
525
+ fromTimestamp: query['timestamp.from'],
526
+ include: query.include.includes('feeToken') ? ['feeToken'] : [],
527
+ limit: query.limit,
528
+ order: query.order,
529
+ page: query.page,
530
+ recipient: query.recipient,
531
+ sender: query.sender,
532
+ status: query.status,
533
+ toBlock: query['blockNumber.to'],
534
+ toTimestamp: query['timestamp.to'],
535
+ })
536
+
537
+ const meta = countPromise ? await countPromise : undefined
538
+ return c.json(
539
+ Response.validated(schema.getReceipts.Response, {
540
+ ...receipts,
541
+ ...(meta ? { meta } : {}),
542
+ }),
543
+ 200,
544
+ )
545
+ } catch (cause) {
546
+ return Response.upstream(c, cause)
547
+ }
548
+ },
549
+ )
550
+ .get(
551
+ '/:transactionHash/receipt',
552
+ Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }),
553
+ OpenApi.validate('param', schema.getReceipt.Params, {
554
+ code: 'transaction_invalid',
555
+ message: 'Invalid transaction hash',
556
+ }),
557
+ OpenApi.validate('query', schema.getReceipt.Query, {
558
+ code: 'query_invalid',
559
+ message: 'Invalid query parameters',
560
+ }),
561
+ OpenApi.describeRoute({
562
+ description: 'Get the outcome of one transaction by its transaction hash.',
563
+ operationId: 'getReceipt',
564
+ responses: OpenApi.responses({
565
+ errors: {
566
+ 404: {
567
+ description: 'No receipt was found for that transaction hash.',
568
+ codes: ['receipt_not_found'],
569
+ },
570
+ 502: 'Could not read receipt data from the upstream JSON-RPC node.',
571
+ },
572
+ success: {
573
+ description: 'A single transaction receipt with status, gas, fees, and logs.',
574
+ schema: schema.getReceipt.Response,
575
+ },
576
+ }),
577
+ summary: 'Get a transaction receipt',
578
+ tags: ['Transactions'],
579
+ }),
580
+ Cache.response<App.Environment>({
581
+ // A receipt exists only once the transaction is mined: the lookup is
582
+ // either a 404 (unknown / not-yet-mined hash) or a final 200, never a
583
+ // pending 200. Default to no-store so the 404 isn't cached with a
584
+ // long-lived header (which would mask the receipt once mined); the handler
585
+ // unconditionally upgrades the 200 to `immutable` below.
586
+ cacheControl: Cache.policies.noStore,
587
+ name: 'tempo-api:receipts:v1',
588
+ key: (c) => Cache.urlKey(c, schema.getReceipt.Query),
589
+ }),
590
+ async (c) => {
591
+ if (Auth.narrowAccess) return Auth.paidAccessError(c)
592
+ if (OpenApi.narrowValidation)
593
+ return OpenApi.validationError(c, {
594
+ code: 'transaction_invalid',
595
+ message: 'Invalid request parameters',
596
+ })
597
+ if (OpenApi.narrowValidation)
598
+ return OpenApi.validationError(c, {
599
+ code: 'query_invalid',
600
+ message: 'Invalid request parameters',
601
+ })
602
+
603
+ const { transactionHash } = c.req.valid('param')
604
+ const query = c.req.valid('query')
605
+ const chainId = query.chainId ?? c.get('chainId')
606
+ const client = c.get('getClient')(chainId)
607
+ // `Schema.Hash` lowercases to a plain string; re-narrow to the hex
608
+ // template literal viem requires at the RPC boundary.
609
+ const hash = transactionHash as Hex.Hex
610
+
611
+ try {
612
+ // Fetch the JSON-RPC receipt; a `null` result means the receipt is
613
+ // unknown or the tx is still pending. The verbatim payload is surfaced
614
+ // under `meta.rpc`, with humanized fields layered on top. Unmined
615
+ // lookups are deliberately never cached — confirmation polling must
616
+ // observe the receipt the moment the node has it.
617
+ const rpc = await Timing.time(c, 'transaction_receipt', () =>
618
+ client.request({ method: 'eth_getTransactionReceipt', params: [hash] }),
619
+ )
620
+ if (!rpc)
621
+ return Response.error(c, {
622
+ code: 'receipt_not_found',
623
+ message: 'Transaction receipt not found',
624
+ status: 404,
625
+ })
626
+
627
+ const data = humanizeReceipt(Response.validated(schema.Rpc.Receipt, rpc))
628
+
629
+ // A receipt is only returned once the transaction is mined, so it is
630
+ // immutable and can be cached aggressively.
631
+ Cache.setPolicy(c, Cache.policies.immutable)
632
+
633
+ // Fee-token metadata is opt-in: resolving it is an extra lookup, so it
634
+ // only runs when requested via `include=feeToken` and the receipt
635
+ // declares a fee token.
636
+ const feeToken = data.feeToken
637
+ if (!query.include.includes('feeToken') || feeToken === undefined)
638
+ return c.json(Response.validated(schema.getReceipt.Response, data), 200)
639
+
640
+ const token = await Timing.time(c, 'fee_token', () =>
641
+ Tokens.resolveToken(c, { address: feeToken, chainId }).catch(() => undefined),
642
+ )
643
+ return c.json(
644
+ Response.validated(
645
+ schema.getReceipt.Response,
646
+ token ? { ...data, meta: { ...data.meta, feeToken: token } } : data,
647
+ ),
648
+ 200,
649
+ )
650
+ } catch (cause) {
651
+ return Response.upstream(c, cause)
652
+ }
653
+ },
654
+ )
655
+ }
656
+
657
+ /**
658
+ * Lists humanized receipts and embeds opt-in `include` resources (currently
659
+ * `feeToken`).
660
+ */
661
+ export async function listReceipts(c: Context<App.Environment>, options: listReceipts.Options) {
662
+ const { include, ...rest } = options
663
+ const page = await getReceipts(c, rest)
664
+ const data = include.includes('feeToken')
665
+ ? await embedFeeTokens(c, page.data, rest.chainId)
666
+ : page.data
667
+ return { ...page, data }
668
+ }
669
+
670
+ export declare namespace listReceipts {
671
+ type Options = getReceipts.Options & {
672
+ /** Optional related resources to embed (e.g. `feeToken`). */
673
+ include: readonly z.output<typeof schema.getReceipt.Include>[]
674
+ }
675
+ }
676
+
677
+ export function getReceipts(c: Context<App.Environment>, options: getReceipts.Options) {
678
+ const {
679
+ address,
680
+ chainId,
681
+ feePayer,
682
+ feeToken,
683
+ fromBlock,
684
+ fromTimestamp,
685
+ limit,
686
+ order,
687
+ recipient,
688
+ sender,
689
+ status,
690
+ toBlock,
691
+ toTimestamp,
692
+ } = options
693
+ const store = c.get('store')
694
+ const tidx = c.get('getTidx')(chainId)
695
+ const sortDirection = order === 'asc' ? 'ASC' : 'DESC'
696
+ // Keyset pagination: anchor the page below the previous row's `(block, tx_idx)`
697
+ // position instead of a numeric offset, so rows arriving at the head can't
698
+ // shift items across pages. A malformed cursor falls back to the head page.
699
+ const cursor = options.cursor ? Cursor.decode(options.cursor, ['int', 'int']) : undefined
700
+ // Bounded positional lane (exclusive with `cursor` at the schema): page 1 is
701
+ // the head page, so it shares the head's SQL and cache entries; deeper pages
702
+ // translate to a SQL OFFSET of `(page - 1) * limit` rows.
703
+ const offset =
704
+ options.page !== undefined && options.page > 1 ? (options.page - 1) * limit : undefined
705
+
706
+ return Timing.time(c, 'receipts', () =>
707
+ Store.memoize(
708
+ async () => {
709
+ // Page the `receipts` table alone, then enrich each row with the
710
+ // `type`/`fee_token` it does not store via a follow-up `txs` lookup
711
+ // (the blocks-feed pattern). Paging straight off `receipts` avoids the
712
+ // `receipts JOIN txs` shape, which TIDX's planner cannot execute under
713
+ // address scope: a `UNION` of two JOINs (or the `(from = X OR to = X)`
714
+ // form) reliably returns `db error` (HTTP 422) for high-traffic
715
+ // addresses. Two index-friendly single-table queries plan cleanly.
716
+ //
717
+ // Build the shared filters once (everything except the address-scope
718
+ // predicate, which is special-cased below).
719
+ const filters: string[] = []
720
+ if (sender !== undefined) filters.push(`"from" = '${sender}'`)
721
+ if (recipient !== undefined) filters.push(`"to" = '${recipient}'`)
722
+ if (feePayer !== undefined) filters.push(`fee_payer = '${feePayer}'`)
723
+ // `fee_token` lives on `txs`, not `receipts`; filter via a subquery so
724
+ // the page query stays single-table.
725
+ if (feeToken !== undefined)
726
+ filters.push(`tx_hash IN (SELECT hash FROM txs WHERE fee_token = '${feeToken}')`)
727
+ if (fromBlock !== undefined) filters.push(`block_num >= ${fromBlock}`)
728
+ if (toBlock !== undefined) filters.push(`block_num <= ${toBlock}`)
729
+ if (fromTimestamp !== undefined) filters.push(`block_timestamp >= '${fromTimestamp}'`)
730
+ if (toTimestamp !== undefined) filters.push(`block_timestamp <= '${toTimestamp}'`)
731
+ if (status !== undefined) filters.push(`status = ${status === 'success' ? 1 : 0}`)
732
+ if (cursor !== undefined)
733
+ filters.push(
734
+ Cursor.keyset([
735
+ { literal: Cursor.literal(cursor[0]!, 'int'), name: 'block_num', order },
736
+ { literal: Cursor.literal(cursor[1]!, 'int'), name: 'tx_idx', order },
737
+ ]),
738
+ )
739
+
740
+ const columns = `block_num, block_timestamp, tx_idx, tx_hash, "from", "to",
741
+ contract_address, gas_used, cumulative_gas_used, effective_gas_price,
742
+ status, fee_payer`
743
+
744
+ // For address-scoped queries, run one branch per side and `UNION` so
745
+ // each branch returns at most `limit + 1` index-ordered rows, the outer
746
+ // query re-merges, and plain `UNION` dedupes the self-receipt case
747
+ // where `from = to = address`. The sort+limit live *inside* each branch
748
+ // so the merge stays bounded.
749
+ //
750
+ // The `receipts` table only indexes `from`, not `to` (a `to` scan over
751
+ // ~25M rows hits the indexer's statement timeout and returns `db error`,
752
+ // HTTP 422). The `from` branch filters `receipts` directly; the `to`
753
+ // branch resolves matching transaction hashes from `txs` (which indexes
754
+ // `to`) and looks them up by the indexed `receipts.tx_hash`.
755
+ const sideQuery = (side: 'from' | 'to', addr: string) => {
756
+ const predicate =
757
+ side === 'from'
758
+ ? `"from" = '${addr}'`
759
+ : `tx_hash IN (SELECT hash FROM txs WHERE "to" = '${addr}')`
760
+ const where = [predicate, ...filters].join(' AND ')
761
+ // In the offset lane each branch over-fetches the full window
762
+ // (`offset + limit + 1` rows) because the skip applies to the merged
763
+ // set: the outer query re-sorts the union and discards the first
764
+ // `offset` rows.
765
+ return `(SELECT ${columns} FROM receipts WHERE ${where} ORDER BY block_num ${sortDirection}, tx_idx ${sortDirection} LIMIT ${(offset ?? 0) + limit + 1})`
766
+ }
767
+ const skip = offset !== undefined ? ` OFFSET ${offset}` : ''
768
+ const query =
769
+ address !== undefined
770
+ ? `
771
+ ${sideQuery('from', address)}
772
+ UNION
773
+ ${sideQuery('to', address)}
774
+ ORDER BY block_num ${sortDirection}, tx_idx ${sortDirection}
775
+ LIMIT ${limit + 1}${skip}
776
+ `
777
+ : `
778
+ SELECT ${columns}
779
+ FROM receipts
780
+ ${filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''}
781
+ ORDER BY block_num ${sortDirection}, tx_idx ${sortDirection}
782
+ LIMIT ${limit + 1}${skip}
783
+ `
784
+
785
+ const result = await tidx.fetch({ chainId, query: query as string })
786
+
787
+ // The next page anchors below the last fetched row's `(block, tx_idx)`.
788
+ const page = Cursor.paginate({
789
+ rows: result.rows,
790
+ limit,
791
+ key: (row) => {
792
+ const block = Value.toNumber(row['block_num'])
793
+ const index = Value.toNumber(row['tx_idx'])
794
+ return block !== undefined && index !== undefined ? [block, index] : undefined
795
+ },
796
+ })
797
+
798
+ // Enrich the page with `type`/`fee_token` from `txs` in a single
799
+ // `IN (...)` lookup scoped to the page's transaction hashes. `txs` is
800
+ // sorted by `(block_num, idx)` with no hash index, so the hash list
801
+ // must be paired with the page's block numbers — a bare `hash IN`
802
+ // full-scans the table.
803
+ const hashes = [
804
+ ...new Set(
805
+ page.rows
806
+ .map((row) => row['tx_hash'])
807
+ .filter((hash): hash is string => typeof hash === 'string'),
808
+ ),
809
+ ]
810
+ const blocks = [
811
+ ...new Set(
812
+ page.rows
813
+ .map((row) => Value.toNumber(row['block_num']))
814
+ .filter((block): block is number => block !== undefined),
815
+ ),
816
+ ]
817
+ const txByHash = new Map<string, { feeToken: unknown; type: unknown }>()
818
+ if (hashes.length > 0 && blocks.length > 0) {
819
+ const txs = await tidx.fetch({
820
+ chainId,
821
+ query: `SELECT hash, type, fee_token FROM txs WHERE block_num IN (${blocks.join(
822
+ ', ',
823
+ )}) AND hash IN (${hashes.map((hash) => `'${hash}'`).join(', ')})` as string,
824
+ })
825
+ for (const row of txs.rows) {
826
+ const hash = row['hash']
827
+ if (typeof hash === 'string')
828
+ txByHash.set(hash, { feeToken: row['fee_token'], type: row['type'] })
829
+ }
830
+ }
831
+
832
+ // Reconstruct the RPC shape from the indexed columns, then humanize it
833
+ // through the same path as the detail endpoint so the list and detail
834
+ // responses are identical. Rows that fail to reconstruct are skipped.
835
+ const data: z.output<typeof schema.Receipt>[] = []
836
+ for (const row of page.rows) {
837
+ const tx = typeof row['tx_hash'] === 'string' ? txByHash.get(row['tx_hash']) : undefined
838
+ const merged = { ...row, fee_token: tx?.feeToken, type: tx?.type }
839
+ const rpc = schema.Rpc.Receipt.safeParse(toRpcReceipt(merged))
840
+ if (rpc.success) data.push(humanizeReceipt(rpc.data))
841
+ }
842
+
843
+ return { data, nextCursor: page.nextCursor }
844
+ },
845
+ {
846
+ key: `receipts:v1:${chainId}:${order}:${address ?? ''}:${sender ?? ''}:${recipient ?? ''}:${feePayer ?? ''}:${feeToken ?? ''}:${fromBlock ?? ''}:${toBlock ?? ''}:${fromTimestamp ?? ''}:${toTimestamp ?? ''}:${status ?? ''}:${cursor ? `cursor:${cursor[0]}:${cursor[1]}` : offset !== undefined ? `page:${options.page}` : 'head'}:${limit}`,
847
+ store,
848
+ ttl: Ttl.seconds(15),
849
+ },
850
+ ),
851
+ )
852
+ }
853
+
854
+ export declare namespace getReceipts {
855
+ type Options = {
856
+ address?: z.output<typeof Schema.Address> | undefined
857
+ chainId: z.output<typeof Schema.ChainId>
858
+ /** Opaque keyset cursor anchoring the page; omit for the head page. */
859
+ cursor?: string | undefined
860
+ feePayer?: z.output<typeof Schema.Address> | undefined
861
+ feeToken?: z.output<typeof Schema.Address> | undefined
862
+ fromBlock?: number | undefined
863
+ fromTimestamp?: string | undefined
864
+ limit: number
865
+ /** 1-indexed page number (positional pagination; exclusive with `cursor`). */
866
+ page?: number | undefined
867
+ order: 'asc' | 'desc'
868
+ recipient?: z.output<typeof Schema.Address> | undefined
869
+ sender?: z.output<typeof Schema.Address> | undefined
870
+ /** Only include receipts whose execution status matches. */
871
+ status?: 'reverted' | 'success' | undefined
872
+ toBlock?: number | undefined
873
+ toTimestamp?: string | undefined
874
+ }
875
+ }
876
+
877
+ /**
878
+ * Builds the non-cursor, non-address-scope filters shared by the receipt page
879
+ * ({@link getReceipts}) and the capped count ({@link countReceipts}), so the
880
+ * count always matches the page it annotates. The address scope is handled
881
+ * separately (its `UNION` workaround differs between page and count).
882
+ */
883
+ function countFilters(options: countReceipts.Options): string[] {
884
+ const filters: string[] = []
885
+ if (options.sender !== undefined) filters.push(`"from" = '${options.sender}'`)
886
+ if (options.recipient !== undefined) filters.push(`"to" = '${options.recipient}'`)
887
+ if (options.feePayer !== undefined) filters.push(`fee_payer = '${options.feePayer}'`)
888
+ if (options.feeToken !== undefined)
889
+ filters.push(`tx_hash IN (SELECT hash FROM txs WHERE fee_token = '${options.feeToken}')`)
890
+ if (options.fromBlock !== undefined) filters.push(`block_num >= ${options.fromBlock}`)
891
+ if (options.toBlock !== undefined) filters.push(`block_num <= ${options.toBlock}`)
892
+ if (options.fromTimestamp !== undefined)
893
+ filters.push(`block_timestamp >= '${options.fromTimestamp}'`)
894
+ if (options.toTimestamp !== undefined) filters.push(`block_timestamp <= '${options.toTimestamp}'`)
895
+ if (options.status !== undefined) filters.push(`status = ${options.status === 'success' ? 1 : 0}`)
896
+ return filters
897
+ }
898
+
899
+ /**
900
+ * Capped total count of receipts matching the same filters as the page. The
901
+ * address scope reuses the page's `UNION` workaround (`receipts` indexes `from`
902
+ * but not `to`, so the `to` side resolves hashes via the `txs` index), counting
903
+ * the deduped keyset so self-receipts (`from = to`) aren't double-counted.
904
+ * `totalCountCapped: true` is a lower bound — at least {@link Schema.countCap} matches.
905
+ */
906
+ export function countReceipts(
907
+ c: Context<App.Environment>,
908
+ options: countReceipts.Options,
909
+ ): Promise<z.output<typeof Schema.CountMeta>> {
910
+ const { address, chainId } = options
911
+ const store = c.get('store')
912
+ const tidx = c.get('getTidx')(chainId)
913
+ return Timing.time(c, 'receipts_count', () =>
914
+ Store.memoize(
915
+ async () => {
916
+ const filters = countFilters(options)
917
+ let query: string
918
+ if (address !== undefined) {
919
+ const fromWhere = [`"from" = '${address}'`, ...filters].join(' AND ')
920
+ const toWhere = [
921
+ `tx_hash IN (SELECT hash FROM txs WHERE "to" = '${address}')`,
922
+ ...filters,
923
+ ].join(' AND ')
924
+ query = `
925
+ SELECT count(*) AS total
926
+ FROM (
927
+ (SELECT block_num, tx_idx FROM receipts WHERE ${fromWhere} LIMIT ${Schema.countCap})
928
+ UNION
929
+ (SELECT block_num, tx_idx FROM receipts WHERE ${toWhere} LIMIT ${Schema.countCap})
930
+ ) AS capped
931
+ `
932
+ } else {
933
+ const where = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''
934
+ query = `
935
+ SELECT count(*) AS total
936
+ FROM (
937
+ SELECT 1 FROM receipts ${where} LIMIT ${Schema.countCap}
938
+ ) AS capped
939
+ `
940
+ }
941
+ const result = await tidx.fetch({ chainId, query: query as string })
942
+ const totalCount = Value.toNumber(result.rows[0]?.['total']) ?? 0
943
+ return { totalCountCapped: totalCount >= Schema.countCap, totalCount }
944
+ },
945
+ {
946
+ key: `receipts-count:v1:${chainId}:${address ?? ''}:${options.sender ?? ''}:${options.recipient ?? ''}:${options.feePayer ?? ''}:${options.feeToken ?? ''}:${options.fromBlock ?? ''}:${options.toBlock ?? ''}:${options.fromTimestamp ?? ''}:${options.toTimestamp ?? ''}:${options.status ?? ''}`,
947
+ store,
948
+ ttl: Ttl.seconds(15),
949
+ },
950
+ ),
951
+ )
952
+ }
953
+
954
+ export declare namespace countReceipts {
955
+ /** Filters for {@link countReceipts}: the page filters minus pagination. */
956
+ type Options = Pick<
957
+ getReceipts.Options,
958
+ | 'address'
959
+ | 'chainId'
960
+ | 'feePayer'
961
+ | 'feeToken'
962
+ | 'fromBlock'
963
+ | 'fromTimestamp'
964
+ | 'recipient'
965
+ | 'sender'
966
+ | 'status'
967
+ | 'toBlock'
968
+ | 'toTimestamp'
969
+ >
970
+ }
971
+
972
+ /**
973
+ * Resolves humanized receipts — including their event logs — for a page of
974
+ * transactions, keyed by lowercased transaction hash. Rows without a receipt
975
+ * (pending, or not yet indexed) are simply absent from the map.
976
+ *
977
+ * Receipt rows come from the indexed `receipts` table (skipped when the caller
978
+ * already paged receipts via the `status` path) and event logs from the raw
979
+ * `logs` table, both fetched once for the whole page. No `Store.memoize` here:
980
+ * the route-level response cache (`Cache.response`) already absorbs repeated
981
+ * requests for the same page.
982
+ */
983
+ export async function embedReceipts(
984
+ c: Context<App.Environment>,
985
+ options: embedReceipts.Options,
986
+ ): Promise<Map<string, z.output<typeof schema.Receipt>>> {
987
+ const { chainId, rows } = options
988
+ const found = new Map<string, z.output<typeof schema.Receipt>>()
989
+
990
+ // Only mined rows can have a receipt; their block numbers also feed the
991
+ // block bound the `logs` lookup requires.
992
+ const blocks: number[] = []
993
+ const hashes = new Set<string>()
994
+ for (const row of rows) {
995
+ if (row.blockNumber === null) continue
996
+ blocks.push(row.blockNumber)
997
+ hashes.add(row.hash.toLowerCase())
998
+ }
999
+ if (hashes.size === 0) return found
1000
+
1001
+ const tidx = c.get('getTidx')(chainId)
1002
+ const inList = [...hashes].map((hash) => `'${hash}'`).join(', ')
1003
+
1004
+ // Raw-table queries (no signature-decoded CTE), so the inline SQL is cast to
1005
+ // `string`. The receipts lookup needs no block bound (`receipts.tx_hash` is
1006
+ // indexed — see the `sideQuery` comment in `getReceipts`); the logs lookup
1007
+ // REQUIRES one: `logs` is sorted by block position, so a bare `tx_hash IN`
1008
+ // scans to genesis (see `blockBound` in `transfers.ts`).
1009
+ const [receipts_result, logs_result] = await Timing.time(c, 'receipts_embed', () =>
1010
+ Promise.all([
1011
+ options.receipts
1012
+ ? undefined
1013
+ : tidx.fetch({
1014
+ chainId,
1015
+ query:
1016
+ `SELECT block_num, block_timestamp, tx_idx, tx_hash, "from", "to", contract_address, gas_used, cumulative_gas_used, effective_gas_price, status, fee_payer FROM receipts WHERE tx_hash IN (${inList})` as string,
1017
+ }),
1018
+ tidx.fetch({
1019
+ chainId,
1020
+ query:
1021
+ `SELECT tx_hash, block_num, tx_idx, log_idx, address, selector, topic1, topic2, topic3, data, block_timestamp FROM logs WHERE block_num BETWEEN ${Math.min(...blocks)} AND ${Math.max(...blocks)} AND tx_hash IN (${inList}) ORDER BY block_num ASC, log_idx ASC` as string,
1022
+ }),
1023
+ ]),
1024
+ )
1025
+
1026
+ // Reconstruct each raw `logs` row into the RPC log shape `schema.Log`
1027
+ // consumes (the postgres `logs` table names `topic0` `selector`), grouped by
1028
+ // transaction hash. Logs that fail to reconstruct are skipped.
1029
+ const logsByHash = new Map<string, z.output<typeof schema.Log>[]>()
1030
+ for (const row of logs_result.rows) {
1031
+ const hash = Value.toText(row['tx_hash'])?.toLowerCase()
1032
+ if (!hash) continue
1033
+ const timestamp = Value.toIsoDateTime(row['block_timestamp'])
1034
+ const log = schema.Log.safeParse({
1035
+ address: row['address'],
1036
+ blockHash: null,
1037
+ blockNumber: Value.toHex(row['block_num']),
1038
+ blockTimestamp: timestamp ? Value.toHex(Math.floor(Date.parse(timestamp) / 1000)) : undefined,
1039
+ data: Value.toText(row['data']) ?? '0x',
1040
+ logIndex: Value.toHex(row['log_idx']),
1041
+ removed: false,
1042
+ topics: [row['selector'], row['topic1'], row['topic2'], row['topic3']].filter(
1043
+ (topic): topic is string => typeof topic === 'string' && topic.length > 0,
1044
+ ),
1045
+ transactionHash: hash,
1046
+ transactionIndex: Value.toHex(row['tx_idx']),
1047
+ })
1048
+ if (!log.success) continue
1049
+ const logs = logsByHash.get(hash)
1050
+ if (logs) logs.push(log.data)
1051
+ else logsByHash.set(hash, [log.data])
1052
+ }
1053
+
1054
+ // Status path: the caller already paged humanized receipts, which the
1055
+ // receipts list reconstructs without logs — rebuild each with the page's
1056
+ // logs attached (both on the row and inside its `meta.rpc` payload).
1057
+ if (options.receipts) {
1058
+ for (const receipt of options.receipts) {
1059
+ const hash = receipt.transactionHash.toLowerCase()
1060
+ const logs = logsByHash.get(hash) ?? []
1061
+ found.set(hash, {
1062
+ ...receipt,
1063
+ logs,
1064
+ meta: { ...receipt.meta, rpc: { ...receipt.meta.rpc, logs } },
1065
+ })
1066
+ }
1067
+ return found
1068
+ }
1069
+
1070
+ const receiptRowByHash = new Map<string, Record<string, unknown>>()
1071
+ for (const row of receipts_result?.rows ?? []) {
1072
+ const hash = Value.toText(row['tx_hash'])?.toLowerCase()
1073
+ if (hash) receiptRowByHash.set(hash, row)
1074
+ }
1075
+
1076
+ // Per transaction row, merge in the `type`/`fee_token` the `receipts` table
1077
+ // does not store from the transaction row itself (saving the `txs` follow-up
1078
+ // lookup the receipts list needs), inject the reconstructed logs, and
1079
+ // humanize through the same path as the receipt endpoints.
1080
+ for (const row of rows) {
1081
+ const hash = row.hash.toLowerCase()
1082
+ const receipt = receiptRowByHash.get(hash)
1083
+ if (!receipt) continue
1084
+ const merged = {
1085
+ ...receipt,
1086
+ fee_token: row.feeToken,
1087
+ // `toRpcReceipt` hex-encodes numeric columns; decode the row's raw hex
1088
+ // `type` byte so it round-trips through the same path.
1089
+ type: Value.hexToNumber(row.meta.rpc.type),
1090
+ }
1091
+ const rpc = schema.Rpc.Receipt.safeParse({
1092
+ ...toRpcReceipt(merged),
1093
+ logs: logsByHash.get(hash) ?? [],
1094
+ })
1095
+ if (rpc.success) found.set(hash, humanizeReceipt(rpc.data))
1096
+ }
1097
+ return found
1098
+ }
1099
+
1100
+ export declare namespace embedReceipts {
1101
+ /** Options for {@link embedReceipts}. */
1102
+ type Options = {
1103
+ /** Target chain id. */
1104
+ chainId: z.output<typeof Schema.ChainId>
1105
+ /**
1106
+ * Pre-fetched humanized receipts for the page (the `status`-filtered
1107
+ * transactions path). When provided, the receipts query is skipped and only
1108
+ * the page's event logs are fetched and reattached.
1109
+ */
1110
+ receipts?: readonly z.output<typeof schema.Receipt>[] | undefined
1111
+ /**
1112
+ * The page's humanized transaction rows. Typed structurally (rather than as
1113
+ * the transactions schema output) so this module never touches the
1114
+ * transaction schemas eagerly across the import cycle.
1115
+ */
1116
+ rows: readonly {
1117
+ /** Block number, or null if pending (pending rows have no receipt). */
1118
+ blockNumber: number | null
1119
+ /** Fee-token preference declared on the transaction. */
1120
+ feeToken?: Hex.Hex | undefined
1121
+ /** Transaction hash. */
1122
+ hash: string
1123
+ /** Verbatim RPC payload carrying the raw `type` byte. */
1124
+ meta: { rpc: { type: Hex.Hex } }
1125
+ }[]
1126
+ }
1127
+ }
1128
+
1129
+ /**
1130
+ * Humanizes a JSON-RPC transaction receipt: decodes `status`/`type`, converts
1131
+ * hex quantities to decimal strings and block fields to numbers, and derives the
1132
+ * block `timestamp` from the top-level `blockTimestamp` (present when
1133
+ * reconstructed from the index) or the (shared) per-log timestamp — `null` when
1134
+ * neither is available. The verbatim payload is preserved under `meta.rpc` and
1135
+ * the raw logs under `logs`. Shared by the receipt detail (RPC source) and list
1136
+ * (reconstructed from the indexer) endpoints, so both expose an identical shape.
1137
+ */
1138
+ export function humanizeReceipt(
1139
+ rpc: z.output<typeof schema.Rpc.Receipt>,
1140
+ ): z.output<typeof schema.Receipt> {
1141
+ const gasUsed = Value.hexToNumber(rpc.gasUsed) ?? 0
1142
+ const effectiveGasPrice = Value.fromHex(rpc.effectiveGasPrice) ?? '0'
1143
+ return {
1144
+ blockHash: rpc.blockHash,
1145
+ blockNumber: Value.hexToNumber(rpc.blockNumber) ?? 0,
1146
+ contractAddress: rpc.contractAddress ?? null,
1147
+ cumulativeGasUsed: Value.hexToNumber(rpc.cumulativeGasUsed) ?? 0,
1148
+ effectiveGasPrice,
1149
+ feeAmount: computeFeeAmount(gasUsed, effectiveGasPrice) ?? '0',
1150
+ feePayer: rpc.feePayer ?? undefined,
1151
+ feeToken: rpc.feeToken ?? undefined,
1152
+ gasUsed,
1153
+ id: rpc.transactionHash,
1154
+ logs: rpc.logs,
1155
+ meta: { rpc },
1156
+ recipient: rpc.to,
1157
+ sender: rpc.from,
1158
+ status: Value.hexToNumber(rpc.status) === 1 ? 'success' : 'reverted',
1159
+ timestamp: Value.hexSecondsToIso(rpc.blockTimestamp ?? rpc.logs[0]?.blockTimestamp) ?? null,
1160
+ transactionHash: rpc.transactionHash,
1161
+ transactionIndex: Value.hexToNumber(rpc.transactionIndex) ?? 0,
1162
+ type: decodeTransactionType(rpc.type),
1163
+ }
1164
+ }
1165
+
1166
+ /**
1167
+ * Re-encodes a TIDX `receipts` row (joined to `txs`) into the JSON-RPC receipt
1168
+ * shape, which becomes the `rpc` payload the list endpoint humanizes through the
1169
+ * same path as the detail endpoint. Numeric columns become `0x` quantities;
1170
+ * fields the indexer does not store — the block hash, logs bloom, and event logs
1171
+ * — are null/empty since the `receipts` table records neither.
1172
+ */
1173
+ export function toRpcReceipt(row: Record<string, unknown>) {
1174
+ const timestamp = Value.toIsoDateTime(row['block_timestamp'])
1175
+ return {
1176
+ blockHash: null,
1177
+ blockNumber: Value.toHex(row['block_num']),
1178
+ blockTimestamp: timestamp ? Value.toHex(Math.floor(Date.parse(timestamp) / 1000)) : undefined,
1179
+ contractAddress: typeof row['contract_address'] === 'string' ? row['contract_address'] : null,
1180
+ cumulativeGasUsed: Value.toHex(row['cumulative_gas_used']),
1181
+ effectiveGasPrice: Value.toHex(row['effective_gas_price']),
1182
+ feePayer: typeof row['fee_payer'] === 'string' ? row['fee_payer'] : undefined,
1183
+ feeToken: typeof row['fee_token'] === 'string' ? row['fee_token'] : undefined,
1184
+ from: row['from'],
1185
+ gasUsed: Value.toHex(row['gas_used']),
1186
+ logs: [],
1187
+ status: Value.toHex(row['status']),
1188
+ to: typeof row['to'] === 'string' ? row['to'] : null,
1189
+ transactionHash: row['tx_hash'],
1190
+ transactionIndex: Value.toHex(row['tx_idx']),
1191
+ type: Value.toHex(row['type']),
1192
+ }
1193
+ }