@subsquid/ponder 0.15.17-sqd.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (654) hide show
  1. package/CHANGELOG.md +3386 -0
  2. package/README.md +186 -0
  3. package/dist/esm/bin/commands/codegen.js +46 -0
  4. package/dist/esm/bin/commands/codegen.js.map +1 -0
  5. package/dist/esm/bin/commands/createViews.js +196 -0
  6. package/dist/esm/bin/commands/createViews.js.map +1 -0
  7. package/dist/esm/bin/commands/dev.js +430 -0
  8. package/dist/esm/bin/commands/dev.js.map +1 -0
  9. package/dist/esm/bin/commands/list.js +148 -0
  10. package/dist/esm/bin/commands/list.js.map +1 -0
  11. package/dist/esm/bin/commands/prune.js +224 -0
  12. package/dist/esm/bin/commands/prune.js.map +1 -0
  13. package/dist/esm/bin/commands/serve.js +198 -0
  14. package/dist/esm/bin/commands/serve.js.map +1 -0
  15. package/dist/esm/bin/commands/start.js +253 -0
  16. package/dist/esm/bin/commands/start.js.map +1 -0
  17. package/dist/esm/bin/isolatedController.js +200 -0
  18. package/dist/esm/bin/isolatedController.js.map +1 -0
  19. package/dist/esm/bin/isolatedWorker.js +146 -0
  20. package/dist/esm/bin/isolatedWorker.js.map +1 -0
  21. package/dist/esm/bin/ponder.js +137 -0
  22. package/dist/esm/bin/ponder.js.map +1 -0
  23. package/dist/esm/bin/utils/codegen.js +25 -0
  24. package/dist/esm/bin/utils/codegen.js.map +1 -0
  25. package/dist/esm/bin/utils/exit.js +100 -0
  26. package/dist/esm/bin/utils/exit.js.map +1 -0
  27. package/dist/esm/build/config.js +743 -0
  28. package/dist/esm/build/config.js.map +1 -0
  29. package/dist/esm/build/factory.js +76 -0
  30. package/dist/esm/build/factory.js.map +1 -0
  31. package/dist/esm/build/index.js +538 -0
  32. package/dist/esm/build/index.js.map +1 -0
  33. package/dist/esm/build/plugin.js +53 -0
  34. package/dist/esm/build/plugin.js.map +1 -0
  35. package/dist/esm/build/pre.js +76 -0
  36. package/dist/esm/build/pre.js.map +1 -0
  37. package/dist/esm/build/schema.js +164 -0
  38. package/dist/esm/build/schema.js.map +1 -0
  39. package/dist/esm/build/stacktrace.js +137 -0
  40. package/dist/esm/build/stacktrace.js.map +1 -0
  41. package/dist/esm/client/index.js +441 -0
  42. package/dist/esm/client/index.js.map +1 -0
  43. package/dist/esm/config/address.js +2 -0
  44. package/dist/esm/config/address.js.map +1 -0
  45. package/dist/esm/config/eventFilter.js +2 -0
  46. package/dist/esm/config/eventFilter.js.map +1 -0
  47. package/dist/esm/config/index.js +2 -0
  48. package/dist/esm/config/index.js.map +1 -0
  49. package/dist/esm/config/utilityTypes.js +2 -0
  50. package/dist/esm/config/utilityTypes.js.map +1 -0
  51. package/dist/esm/database/actions.js +445 -0
  52. package/dist/esm/database/actions.js.map +1 -0
  53. package/dist/esm/database/index.js +597 -0
  54. package/dist/esm/database/index.js.map +1 -0
  55. package/dist/esm/database/queryBuilder.js +310 -0
  56. package/dist/esm/database/queryBuilder.js.map +1 -0
  57. package/dist/esm/drizzle/bigint.js +38 -0
  58. package/dist/esm/drizzle/bigint.js.map +1 -0
  59. package/dist/esm/drizzle/bytes.js +47 -0
  60. package/dist/esm/drizzle/bytes.js.map +1 -0
  61. package/dist/esm/drizzle/hex.js +40 -0
  62. package/dist/esm/drizzle/hex.js.map +1 -0
  63. package/dist/esm/drizzle/index.js +40 -0
  64. package/dist/esm/drizzle/index.js.map +1 -0
  65. package/dist/esm/drizzle/json.js +119 -0
  66. package/dist/esm/drizzle/json.js.map +1 -0
  67. package/dist/esm/drizzle/kit/index.js +928 -0
  68. package/dist/esm/drizzle/kit/index.js.map +1 -0
  69. package/dist/esm/drizzle/onchain.js +158 -0
  70. package/dist/esm/drizzle/onchain.js.map +1 -0
  71. package/dist/esm/drizzle/text.js +61 -0
  72. package/dist/esm/drizzle/text.js.map +1 -0
  73. package/dist/esm/graphql/graphiql.html.js +59 -0
  74. package/dist/esm/graphql/graphiql.html.js.map +1 -0
  75. package/dist/esm/graphql/index.js +916 -0
  76. package/dist/esm/graphql/index.js.map +1 -0
  77. package/dist/esm/graphql/json.js +42 -0
  78. package/dist/esm/graphql/json.js.map +1 -0
  79. package/dist/esm/graphql/middleware.js +78 -0
  80. package/dist/esm/graphql/middleware.js.map +1 -0
  81. package/dist/esm/index.js +9 -0
  82. package/dist/esm/index.js.map +1 -0
  83. package/dist/esm/indexing/addStackTrace.js +54 -0
  84. package/dist/esm/indexing/addStackTrace.js.map +1 -0
  85. package/dist/esm/indexing/client.js +675 -0
  86. package/dist/esm/indexing/client.js.map +1 -0
  87. package/dist/esm/indexing/index.js +652 -0
  88. package/dist/esm/indexing/index.js.map +1 -0
  89. package/dist/esm/indexing/profile.js +584 -0
  90. package/dist/esm/indexing/profile.js.map +1 -0
  91. package/dist/esm/indexing-store/cache.js +665 -0
  92. package/dist/esm/indexing-store/cache.js.map +1 -0
  93. package/dist/esm/indexing-store/historical.js +427 -0
  94. package/dist/esm/indexing-store/historical.js.map +1 -0
  95. package/dist/esm/indexing-store/index.js +35 -0
  96. package/dist/esm/indexing-store/index.js.map +1 -0
  97. package/dist/esm/indexing-store/profile.js +428 -0
  98. package/dist/esm/indexing-store/profile.js.map +1 -0
  99. package/dist/esm/indexing-store/realtime.js +305 -0
  100. package/dist/esm/indexing-store/realtime.js.map +1 -0
  101. package/dist/esm/indexing-store/utils.js +111 -0
  102. package/dist/esm/indexing-store/utils.js.map +1 -0
  103. package/dist/esm/internal/common.js +2 -0
  104. package/dist/esm/internal/common.js.map +1 -0
  105. package/dist/esm/internal/errors.js +300 -0
  106. package/dist/esm/internal/errors.js.map +1 -0
  107. package/dist/esm/internal/logger.js +178 -0
  108. package/dist/esm/internal/logger.js.map +1 -0
  109. package/dist/esm/internal/metrics.js +1046 -0
  110. package/dist/esm/internal/metrics.js.map +1 -0
  111. package/dist/esm/internal/options.js +73 -0
  112. package/dist/esm/internal/options.js.map +1 -0
  113. package/dist/esm/internal/shutdown.js +24 -0
  114. package/dist/esm/internal/shutdown.js.map +1 -0
  115. package/dist/esm/internal/telemetry.js +200 -0
  116. package/dist/esm/internal/telemetry.js.map +1 -0
  117. package/dist/esm/internal/types.js +2 -0
  118. package/dist/esm/internal/types.js.map +1 -0
  119. package/dist/esm/rpc/actions.js +988 -0
  120. package/dist/esm/rpc/actions.js.map +1 -0
  121. package/dist/esm/rpc/http.js +130 -0
  122. package/dist/esm/rpc/http.js.map +1 -0
  123. package/dist/esm/rpc/index.js +749 -0
  124. package/dist/esm/rpc/index.js.map +1 -0
  125. package/dist/esm/runtime/events.js +664 -0
  126. package/dist/esm/runtime/events.js.map +1 -0
  127. package/dist/esm/runtime/filter.js +443 -0
  128. package/dist/esm/runtime/filter.js.map +1 -0
  129. package/dist/esm/runtime/fragments.js +478 -0
  130. package/dist/esm/runtime/fragments.js.map +1 -0
  131. package/dist/esm/runtime/historical.js +985 -0
  132. package/dist/esm/runtime/historical.js.map +1 -0
  133. package/dist/esm/runtime/index.js +325 -0
  134. package/dist/esm/runtime/index.js.map +1 -0
  135. package/dist/esm/runtime/init.js +12 -0
  136. package/dist/esm/runtime/init.js.map +1 -0
  137. package/dist/esm/runtime/isolated.js +463 -0
  138. package/dist/esm/runtime/isolated.js.map +1 -0
  139. package/dist/esm/runtime/multichain.js +509 -0
  140. package/dist/esm/runtime/multichain.js.map +1 -0
  141. package/dist/esm/runtime/omnichain.js +544 -0
  142. package/dist/esm/runtime/omnichain.js.map +1 -0
  143. package/dist/esm/runtime/realtime.js +733 -0
  144. package/dist/esm/runtime/realtime.js.map +1 -0
  145. package/dist/esm/server/error.js +56 -0
  146. package/dist/esm/server/error.js.map +1 -0
  147. package/dist/esm/server/index.js +121 -0
  148. package/dist/esm/server/index.js.map +1 -0
  149. package/dist/esm/sync-historical/index.js +701 -0
  150. package/dist/esm/sync-historical/index.js.map +1 -0
  151. package/dist/esm/sync-historical/portal-realtime-wire.js +302 -0
  152. package/dist/esm/sync-historical/portal-realtime-wire.js.map +1 -0
  153. package/dist/esm/sync-historical/portal-realtime.js +154 -0
  154. package/dist/esm/sync-historical/portal-realtime.js.map +1 -0
  155. package/dist/esm/sync-historical/portal-transform.js +113 -0
  156. package/dist/esm/sync-historical/portal-transform.js.map +1 -0
  157. package/dist/esm/sync-historical/portal.js +949 -0
  158. package/dist/esm/sync-historical/portal.js.map +1 -0
  159. package/dist/esm/sync-historical/realtime.js +127 -0
  160. package/dist/esm/sync-historical/realtime.js.map +1 -0
  161. package/dist/esm/sync-realtime/bloom.js +76 -0
  162. package/dist/esm/sync-realtime/bloom.js.map +1 -0
  163. package/dist/esm/sync-realtime/index.js +917 -0
  164. package/dist/esm/sync-realtime/index.js.map +1 -0
  165. package/dist/esm/sync-store/encode.js +105 -0
  166. package/dist/esm/sync-store/encode.js.map +1 -0
  167. package/dist/esm/sync-store/index.js +885 -0
  168. package/dist/esm/sync-store/index.js.map +1 -0
  169. package/dist/esm/sync-store/migrations.js +1595 -0
  170. package/dist/esm/sync-store/migrations.js.map +1 -0
  171. package/dist/esm/sync-store/schema.js +181 -0
  172. package/dist/esm/sync-store/schema.js.map +1 -0
  173. package/dist/esm/types/db.js +2 -0
  174. package/dist/esm/types/db.js.map +1 -0
  175. package/dist/esm/types/eth.js +2 -0
  176. package/dist/esm/types/eth.js.map +1 -0
  177. package/dist/esm/types/utils.js +2 -0
  178. package/dist/esm/types/utils.js.map +1 -0
  179. package/dist/esm/types/virtual.js +2 -0
  180. package/dist/esm/types/virtual.js.map +1 -0
  181. package/dist/esm/ui/app.js +157 -0
  182. package/dist/esm/ui/app.js.map +1 -0
  183. package/dist/esm/ui/index.js +29 -0
  184. package/dist/esm/ui/index.js.map +1 -0
  185. package/dist/esm/ui/patch.js +103 -0
  186. package/dist/esm/ui/patch.js.map +1 -0
  187. package/dist/esm/utils/abi.js +55 -0
  188. package/dist/esm/utils/abi.js.map +1 -0
  189. package/dist/esm/utils/bigint.js +37 -0
  190. package/dist/esm/utils/bigint.js.map +1 -0
  191. package/dist/esm/utils/chains.js +21 -0
  192. package/dist/esm/utils/chains.js.map +1 -0
  193. package/dist/esm/utils/checkpoint.js +139 -0
  194. package/dist/esm/utils/checkpoint.js.map +1 -0
  195. package/dist/esm/utils/chunk.js +8 -0
  196. package/dist/esm/utils/chunk.js.map +1 -0
  197. package/dist/esm/utils/copy.js +129 -0
  198. package/dist/esm/utils/copy.js.map +1 -0
  199. package/dist/esm/utils/date.js +27 -0
  200. package/dist/esm/utils/date.js.map +1 -0
  201. package/dist/esm/utils/debug.js +2 -0
  202. package/dist/esm/utils/debug.js.map +1 -0
  203. package/dist/esm/utils/decodeAbiParameters.js +290 -0
  204. package/dist/esm/utils/decodeAbiParameters.js.map +1 -0
  205. package/dist/esm/utils/decodeEventLog.js +75 -0
  206. package/dist/esm/utils/decodeEventLog.js.map +1 -0
  207. package/dist/esm/utils/dedupe.js +29 -0
  208. package/dist/esm/utils/dedupe.js.map +1 -0
  209. package/dist/esm/utils/duplicates.js +19 -0
  210. package/dist/esm/utils/duplicates.js.map +1 -0
  211. package/dist/esm/utils/estimate.js +6 -0
  212. package/dist/esm/utils/estimate.js.map +1 -0
  213. package/dist/esm/utils/finality.js +38 -0
  214. package/dist/esm/utils/finality.js.map +1 -0
  215. package/dist/esm/utils/format.js +20 -0
  216. package/dist/esm/utils/format.js.map +1 -0
  217. package/dist/esm/utils/generators.js +121 -0
  218. package/dist/esm/utils/generators.js.map +1 -0
  219. package/dist/esm/utils/hash.js +11 -0
  220. package/dist/esm/utils/hash.js.map +1 -0
  221. package/dist/esm/utils/interval.js +171 -0
  222. package/dist/esm/utils/interval.js.map +1 -0
  223. package/dist/esm/utils/lowercase.js +7 -0
  224. package/dist/esm/utils/lowercase.js.map +1 -0
  225. package/dist/esm/utils/mutex.js +26 -0
  226. package/dist/esm/utils/mutex.js.map +1 -0
  227. package/dist/esm/utils/never.js +4 -0
  228. package/dist/esm/utils/never.js.map +1 -0
  229. package/dist/esm/utils/offset.js +101 -0
  230. package/dist/esm/utils/offset.js.map +1 -0
  231. package/dist/esm/utils/order.js +18 -0
  232. package/dist/esm/utils/order.js.map +1 -0
  233. package/dist/esm/utils/partition.js +46 -0
  234. package/dist/esm/utils/partition.js.map +1 -0
  235. package/dist/esm/utils/pg.js +149 -0
  236. package/dist/esm/utils/pg.js.map +1 -0
  237. package/dist/esm/utils/pglite.js +80 -0
  238. package/dist/esm/utils/pglite.js.map +1 -0
  239. package/dist/esm/utils/port.js +30 -0
  240. package/dist/esm/utils/port.js.map +1 -0
  241. package/dist/esm/utils/print.js +23 -0
  242. package/dist/esm/utils/print.js.map +1 -0
  243. package/dist/esm/utils/promiseAllSettledWithThrow.js +19 -0
  244. package/dist/esm/utils/promiseAllSettledWithThrow.js.map +1 -0
  245. package/dist/esm/utils/promiseWithResolvers.js +13 -0
  246. package/dist/esm/utils/promiseWithResolvers.js.map +1 -0
  247. package/dist/esm/utils/queue.js +150 -0
  248. package/dist/esm/utils/queue.js.map +1 -0
  249. package/dist/esm/utils/range.js +8 -0
  250. package/dist/esm/utils/range.js.map +1 -0
  251. package/dist/esm/utils/result.js +10 -0
  252. package/dist/esm/utils/result.js.map +1 -0
  253. package/dist/esm/utils/sql-parse.js +1326 -0
  254. package/dist/esm/utils/sql-parse.js.map +1 -0
  255. package/dist/esm/utils/timer.js +9 -0
  256. package/dist/esm/utils/timer.js.map +1 -0
  257. package/dist/esm/utils/truncate.js +15 -0
  258. package/dist/esm/utils/truncate.js.map +1 -0
  259. package/dist/esm/utils/wait.js +10 -0
  260. package/dist/esm/utils/wait.js.map +1 -0
  261. package/dist/esm/utils/zipper.js +67 -0
  262. package/dist/esm/utils/zipper.js.map +1 -0
  263. package/dist/types/bin/commands/codegen.d.ts +5 -0
  264. package/dist/types/bin/commands/codegen.d.ts.map +1 -0
  265. package/dist/types/bin/commands/createViews.d.ts +8 -0
  266. package/dist/types/bin/commands/createViews.d.ts.map +1 -0
  267. package/dist/types/bin/commands/dev.d.ts +5 -0
  268. package/dist/types/bin/commands/dev.d.ts.map +1 -0
  269. package/dist/types/bin/commands/list.d.ts +5 -0
  270. package/dist/types/bin/commands/list.d.ts.map +1 -0
  271. package/dist/types/bin/commands/prune.d.ts +5 -0
  272. package/dist/types/bin/commands/prune.d.ts.map +1 -0
  273. package/dist/types/bin/commands/serve.d.ts +5 -0
  274. package/dist/types/bin/commands/serve.d.ts.map +1 -0
  275. package/dist/types/bin/commands/start.d.ts +19 -0
  276. package/dist/types/bin/commands/start.d.ts.map +1 -0
  277. package/dist/types/bin/isolatedController.d.ts +13 -0
  278. package/dist/types/bin/isolatedController.d.ts.map +1 -0
  279. package/dist/types/bin/isolatedWorker.d.ts +9 -0
  280. package/dist/types/bin/isolatedWorker.d.ts.map +1 -0
  281. package/dist/types/bin/ponder.d.ts +37 -0
  282. package/dist/types/bin/ponder.d.ts.map +1 -0
  283. package/dist/types/bin/utils/codegen.d.ts +6 -0
  284. package/dist/types/bin/utils/codegen.d.ts.map +1 -0
  285. package/dist/types/bin/utils/exit.d.ts +10 -0
  286. package/dist/types/bin/utils/exit.d.ts.map +1 -0
  287. package/dist/types/build/config.d.ts +97 -0
  288. package/dist/types/build/config.d.ts.map +1 -0
  289. package/dist/types/build/factory.d.ts +13 -0
  290. package/dist/types/build/factory.d.ts.map +1 -0
  291. package/dist/types/build/index.d.ts +84 -0
  292. package/dist/types/build/index.d.ts.map +1 -0
  293. package/dist/types/build/plugin.d.ts +4 -0
  294. package/dist/types/build/plugin.d.ts.map +1 -0
  295. package/dist/types/build/pre.d.ts +26 -0
  296. package/dist/types/build/pre.d.ts.map +1 -0
  297. package/dist/types/build/schema.d.ts +20 -0
  298. package/dist/types/build/schema.d.ts.map +1 -0
  299. package/dist/types/build/stacktrace.d.ts +13 -0
  300. package/dist/types/build/stacktrace.d.ts.map +1 -0
  301. package/dist/types/client/index.d.ts +27 -0
  302. package/dist/types/client/index.d.ts.map +1 -0
  303. package/dist/types/config/address.d.ts +24 -0
  304. package/dist/types/config/address.d.ts.map +1 -0
  305. package/dist/types/config/eventFilter.d.ts +18 -0
  306. package/dist/types/config/eventFilter.d.ts.map +1 -0
  307. package/dist/types/config/index.d.ts +149 -0
  308. package/dist/types/config/index.d.ts.map +1 -0
  309. package/dist/types/config/utilityTypes.d.ts +43 -0
  310. package/dist/types/config/utilityTypes.d.ts.map +1 -0
  311. package/dist/types/database/actions.d.ts +99 -0
  312. package/dist/types/database/actions.d.ts.map +1 -0
  313. package/dist/types/database/index.d.ts +481 -0
  314. package/dist/types/database/index.d.ts.map +1 -0
  315. package/dist/types/database/queryBuilder.d.ts +65 -0
  316. package/dist/types/database/queryBuilder.d.ts.map +1 -0
  317. package/dist/types/drizzle/bigint.d.ts +25 -0
  318. package/dist/types/drizzle/bigint.d.ts.map +1 -0
  319. package/dist/types/drizzle/bytes.d.ts +31 -0
  320. package/dist/types/drizzle/bytes.d.ts.map +1 -0
  321. package/dist/types/drizzle/hex.d.ts +25 -0
  322. package/dist/types/drizzle/hex.d.ts.map +1 -0
  323. package/dist/types/drizzle/index.d.ts +10 -0
  324. package/dist/types/drizzle/index.d.ts.map +1 -0
  325. package/dist/types/drizzle/json.d.ts +51 -0
  326. package/dist/types/drizzle/json.d.ts.map +1 -0
  327. package/dist/types/drizzle/kit/index.d.ts +189 -0
  328. package/dist/types/drizzle/kit/index.d.ts.map +1 -0
  329. package/dist/types/drizzle/onchain.d.ts +287 -0
  330. package/dist/types/drizzle/onchain.d.ts.map +1 -0
  331. package/dist/types/drizzle/text.d.ts +29 -0
  332. package/dist/types/drizzle/text.d.ts.map +1 -0
  333. package/dist/types/graphql/graphiql.html.d.ts +2 -0
  334. package/dist/types/graphql/graphiql.html.d.ts.map +1 -0
  335. package/dist/types/graphql/index.d.ts +12 -0
  336. package/dist/types/graphql/index.d.ts.map +1 -0
  337. package/dist/types/graphql/json.d.ts +3 -0
  338. package/dist/types/graphql/json.d.ts.map +1 -0
  339. package/dist/types/graphql/middleware.d.ts +29 -0
  340. package/dist/types/graphql/middleware.d.ts.map +1 -0
  341. package/dist/types/index.d.ts +23 -0
  342. package/dist/types/index.d.ts.map +1 -0
  343. package/dist/types/indexing/addStackTrace.d.ts +3 -0
  344. package/dist/types/indexing/addStackTrace.d.ts.map +1 -0
  345. package/dist/types/indexing/client.d.ts +154 -0
  346. package/dist/types/indexing/client.d.ts.map +1 -0
  347. package/dist/types/indexing/index.d.ts +75 -0
  348. package/dist/types/indexing/index.d.ts.map +1 -0
  349. package/dist/types/indexing/profile.d.ts +16 -0
  350. package/dist/types/indexing/profile.d.ts.map +1 -0
  351. package/dist/types/indexing-store/cache.d.ts +115 -0
  352. package/dist/types/indexing-store/cache.d.ts.map +1 -0
  353. package/dist/types/indexing-store/historical.d.ts +12 -0
  354. package/dist/types/indexing-store/historical.d.ts.map +1 -0
  355. package/dist/types/indexing-store/index.d.ts +14 -0
  356. package/dist/types/indexing-store/index.d.ts.map +1 -0
  357. package/dist/types/indexing-store/profile.d.ts +7 -0
  358. package/dist/types/indexing-store/profile.d.ts.map +1 -0
  359. package/dist/types/indexing-store/realtime.d.ts +10 -0
  360. package/dist/types/indexing-store/realtime.d.ts.map +1 -0
  361. package/dist/types/indexing-store/utils.d.ts +19 -0
  362. package/dist/types/indexing-store/utils.d.ts.map +1 -0
  363. package/dist/types/internal/common.d.ts +15 -0
  364. package/dist/types/internal/common.d.ts.map +1 -0
  365. package/dist/types/internal/errors.d.ts +101 -0
  366. package/dist/types/internal/errors.d.ts.map +1 -0
  367. package/dist/types/internal/logger.d.ts +37 -0
  368. package/dist/types/internal/logger.d.ts.map +1 -0
  369. package/dist/types/internal/metrics.d.ts +120 -0
  370. package/dist/types/internal/metrics.d.ts.map +1 -0
  371. package/dist/types/internal/options.d.ts +62 -0
  372. package/dist/types/internal/options.d.ts.map +1 -0
  373. package/dist/types/internal/shutdown.d.ts +8 -0
  374. package/dist/types/internal/shutdown.d.ts.map +1 -0
  375. package/dist/types/internal/telemetry.d.ts +43 -0
  376. package/dist/types/internal/telemetry.d.ts.map +1 -0
  377. package/dist/types/internal/types.d.ts +435 -0
  378. package/dist/types/internal/types.d.ts.map +1 -0
  379. package/dist/types/rpc/actions.d.ts +360 -0
  380. package/dist/types/rpc/actions.d.ts.map +1 -0
  381. package/dist/types/rpc/http.d.ts +17 -0
  382. package/dist/types/rpc/http.d.ts.map +1 -0
  383. package/dist/types/rpc/index.d.ts +43 -0
  384. package/dist/types/rpc/index.d.ts.map +1 -0
  385. package/dist/types/runtime/events.d.ts +40 -0
  386. package/dist/types/runtime/events.d.ts.map +1 -0
  387. package/dist/types/runtime/filter.d.ts +87 -0
  388. package/dist/types/runtime/filter.d.ts.map +1 -0
  389. package/dist/types/runtime/fragments.d.ts +30 -0
  390. package/dist/types/runtime/fragments.d.ts.map +1 -0
  391. package/dist/types/runtime/historical.d.ts +123 -0
  392. package/dist/types/runtime/historical.d.ts.map +1 -0
  393. package/dist/types/runtime/index.d.ts +89 -0
  394. package/dist/types/runtime/index.d.ts.map +1 -0
  395. package/dist/types/runtime/init.d.ts +28 -0
  396. package/dist/types/runtime/init.d.ts.map +1 -0
  397. package/dist/types/runtime/isolated.d.ts +14 -0
  398. package/dist/types/runtime/isolated.d.ts.map +1 -0
  399. package/dist/types/runtime/multichain.d.ts +13 -0
  400. package/dist/types/runtime/multichain.d.ts.map +1 -0
  401. package/dist/types/runtime/omnichain.d.ts +23 -0
  402. package/dist/types/runtime/omnichain.d.ts.map +1 -0
  403. package/dist/types/runtime/realtime.d.ts +93 -0
  404. package/dist/types/runtime/realtime.d.ts.map +1 -0
  405. package/dist/types/server/error.d.ts +5 -0
  406. package/dist/types/server/error.d.ts.map +1 -0
  407. package/dist/types/server/index.d.ts +13 -0
  408. package/dist/types/server/index.d.ts.map +1 -0
  409. package/dist/types/sync-historical/index.d.ts +36 -0
  410. package/dist/types/sync-historical/index.d.ts.map +1 -0
  411. package/dist/types/sync-historical/portal-realtime-wire.d.ts +102 -0
  412. package/dist/types/sync-historical/portal-realtime-wire.d.ts.map +1 -0
  413. package/dist/types/sync-historical/portal-realtime.d.ts +95 -0
  414. package/dist/types/sync-historical/portal-realtime.d.ts.map +1 -0
  415. package/dist/types/sync-historical/portal-transform.d.ts +51 -0
  416. package/dist/types/sync-historical/portal-transform.d.ts.map +1 -0
  417. package/dist/types/sync-historical/portal.d.ts +34 -0
  418. package/dist/types/sync-historical/portal.d.ts.map +1 -0
  419. package/dist/types/sync-historical/realtime.d.ts +71 -0
  420. package/dist/types/sync-historical/realtime.d.ts.map +1 -0
  421. package/dist/types/sync-realtime/bloom.d.ts +18 -0
  422. package/dist/types/sync-realtime/bloom.d.ts.map +1 -0
  423. package/dist/types/sync-realtime/index.d.ts +47 -0
  424. package/dist/types/sync-realtime/index.d.ts.map +1 -0
  425. package/dist/types/sync-store/encode.d.ts +25 -0
  426. package/dist/types/sync-store/encode.d.ts.map +1 -0
  427. package/dist/types/sync-store/index.d.ts +135 -0
  428. package/dist/types/sync-store/index.d.ts.map +1 -0
  429. package/dist/types/sync-store/migrations.d.ts +8 -0
  430. package/dist/types/sync-store/migrations.d.ts.map +1 -0
  431. package/dist/types/sync-store/schema.d.ts +1828 -0
  432. package/dist/types/sync-store/schema.d.ts.map +1 -0
  433. package/dist/types/types/db.d.ts +213 -0
  434. package/dist/types/types/db.d.ts.map +1 -0
  435. package/dist/types/types/eth.d.ts +196 -0
  436. package/dist/types/types/eth.d.ts.map +1 -0
  437. package/dist/types/types/utils.d.ts +38 -0
  438. package/dist/types/types/utils.d.ts.map +1 -0
  439. package/dist/types/types/virtual.d.ts +99 -0
  440. package/dist/types/types/virtual.d.ts.map +1 -0
  441. package/dist/types/ui/app.d.ts +22 -0
  442. package/dist/types/ui/app.d.ts.map +1 -0
  443. package/dist/types/ui/index.d.ts +5 -0
  444. package/dist/types/ui/index.d.ts.map +1 -0
  445. package/dist/types/ui/patch.d.ts +7 -0
  446. package/dist/types/ui/patch.d.ts.map +1 -0
  447. package/dist/types/utils/abi.d.ts +23 -0
  448. package/dist/types/utils/abi.d.ts.map +1 -0
  449. package/dist/types/utils/bigint.d.ts +15 -0
  450. package/dist/types/utils/bigint.d.ts.map +1 -0
  451. package/dist/types/utils/chains.d.ts +42 -0
  452. package/dist/types/utils/chains.d.ts.map +1 -0
  453. package/dist/types/utils/checkpoint.d.ts +52 -0
  454. package/dist/types/utils/checkpoint.d.ts.map +1 -0
  455. package/dist/types/utils/chunk.d.ts +2 -0
  456. package/dist/types/utils/chunk.d.ts.map +1 -0
  457. package/dist/types/utils/copy.d.ts +16 -0
  458. package/dist/types/utils/copy.d.ts.map +1 -0
  459. package/dist/types/utils/date.d.ts +7 -0
  460. package/dist/types/utils/date.d.ts.map +1 -0
  461. package/dist/types/utils/debug.d.ts +105 -0
  462. package/dist/types/utils/debug.d.ts.map +1 -0
  463. package/dist/types/utils/decodeAbiParameters.d.ts +28 -0
  464. package/dist/types/utils/decodeAbiParameters.d.ts.map +1 -0
  465. package/dist/types/utils/decodeEventLog.d.ts +12 -0
  466. package/dist/types/utils/decodeEventLog.d.ts.map +1 -0
  467. package/dist/types/utils/dedupe.d.ts +20 -0
  468. package/dist/types/utils/dedupe.d.ts.map +1 -0
  469. package/dist/types/utils/duplicates.d.ts +7 -0
  470. package/dist/types/utils/duplicates.d.ts.map +1 -0
  471. package/dist/types/utils/estimate.d.ts +11 -0
  472. package/dist/types/utils/estimate.d.ts.map +1 -0
  473. package/dist/types/utils/finality.d.ts +12 -0
  474. package/dist/types/utils/finality.d.ts.map +1 -0
  475. package/dist/types/utils/format.d.ts +3 -0
  476. package/dist/types/utils/format.d.ts.map +1 -0
  477. package/dist/types/utils/generators.d.ts +42 -0
  478. package/dist/types/utils/generators.d.ts.map +1 -0
  479. package/dist/types/utils/hash.d.ts +11 -0
  480. package/dist/types/utils/hash.d.ts.map +1 -0
  481. package/dist/types/utils/interval.d.ts +53 -0
  482. package/dist/types/utils/interval.d.ts.map +1 -0
  483. package/dist/types/utils/lowercase.d.ts +5 -0
  484. package/dist/types/utils/lowercase.d.ts.map +1 -0
  485. package/dist/types/utils/mutex.d.ts +5 -0
  486. package/dist/types/utils/mutex.d.ts.map +1 -0
  487. package/dist/types/utils/never.d.ts +2 -0
  488. package/dist/types/utils/never.d.ts.map +1 -0
  489. package/dist/types/utils/offset.d.ts +8 -0
  490. package/dist/types/utils/offset.d.ts.map +1 -0
  491. package/dist/types/utils/order.d.ts +2 -0
  492. package/dist/types/utils/order.d.ts.map +1 -0
  493. package/dist/types/utils/partition.d.ts +22 -0
  494. package/dist/types/utils/partition.d.ts.map +1 -0
  495. package/dist/types/utils/pg.d.ts +8 -0
  496. package/dist/types/utils/pg.d.ts.map +1 -0
  497. package/dist/types/utils/pglite.d.ts +25 -0
  498. package/dist/types/utils/pglite.d.ts.map +1 -0
  499. package/dist/types/utils/port.d.ts +5 -0
  500. package/dist/types/utils/port.d.ts.map +1 -0
  501. package/dist/types/utils/print.d.ts +2 -0
  502. package/dist/types/utils/print.d.ts.map +1 -0
  503. package/dist/types/utils/promiseAllSettledWithThrow.d.ts +8 -0
  504. package/dist/types/utils/promiseAllSettledWithThrow.d.ts.map +1 -0
  505. package/dist/types/utils/promiseWithResolvers.d.ts +10 -0
  506. package/dist/types/utils/promiseWithResolvers.d.ts.map +1 -0
  507. package/dist/types/utils/queue.d.ts +33 -0
  508. package/dist/types/utils/queue.d.ts.map +1 -0
  509. package/dist/types/utils/range.d.ts +8 -0
  510. package/dist/types/utils/range.d.ts.map +1 -0
  511. package/dist/types/utils/result.d.ts +17 -0
  512. package/dist/types/utils/result.d.ts.map +1 -0
  513. package/dist/types/utils/sql-parse.d.ts +21 -0
  514. package/dist/types/utils/sql-parse.d.ts.map +1 -0
  515. package/dist/types/utils/timer.d.ts +6 -0
  516. package/dist/types/utils/timer.d.ts.map +1 -0
  517. package/dist/types/utils/truncate.d.ts +9 -0
  518. package/dist/types/utils/truncate.d.ts.map +1 -0
  519. package/dist/types/utils/wait.d.ts +6 -0
  520. package/dist/types/utils/wait.d.ts.map +1 -0
  521. package/dist/types/utils/zipper.d.ts +36 -0
  522. package/dist/types/utils/zipper.d.ts.map +1 -0
  523. package/package.json +116 -0
  524. package/src/bin/commands/codegen.ts +56 -0
  525. package/src/bin/commands/createViews.ts +311 -0
  526. package/src/bin/commands/dev.ts +490 -0
  527. package/src/bin/commands/list.ts +207 -0
  528. package/src/bin/commands/prune.ts +316 -0
  529. package/src/bin/commands/serve.ts +236 -0
  530. package/src/bin/commands/start.ts +319 -0
  531. package/src/bin/isolatedController.ts +300 -0
  532. package/src/bin/isolatedWorker.ts +192 -0
  533. package/src/bin/ponder.ts +200 -0
  534. package/src/bin/utils/codegen.ts +32 -0
  535. package/src/bin/utils/exit.ts +112 -0
  536. package/src/build/config.ts +1136 -0
  537. package/src/build/factory.ts +122 -0
  538. package/src/build/index.ts +747 -0
  539. package/src/build/plugin.ts +58 -0
  540. package/src/build/pre.ts +100 -0
  541. package/src/build/schema.ts +291 -0
  542. package/src/build/stacktrace.ts +137 -0
  543. package/src/client/index.ts +551 -0
  544. package/src/config/address.ts +32 -0
  545. package/src/config/eventFilter.ts +33 -0
  546. package/src/config/index.ts +245 -0
  547. package/src/config/utilityTypes.ts +152 -0
  548. package/src/database/actions.ts +870 -0
  549. package/src/database/index.ts +1018 -0
  550. package/src/database/queryBuilder.ts +534 -0
  551. package/src/drizzle/bigint.ts +57 -0
  552. package/src/drizzle/bytes.ts +68 -0
  553. package/src/drizzle/hex.ts +58 -0
  554. package/src/drizzle/index.ts +58 -0
  555. package/src/drizzle/json.ts +154 -0
  556. package/src/drizzle/kit/index.ts +1352 -0
  557. package/src/drizzle/onchain.ts +447 -0
  558. package/src/drizzle/text.ts +77 -0
  559. package/src/graphql/graphiql.html.ts +59 -0
  560. package/src/graphql/index.ts +1329 -0
  561. package/src/graphql/json.ts +62 -0
  562. package/src/graphql/middleware.ts +111 -0
  563. package/src/index.ts +139 -0
  564. package/src/indexing/addStackTrace.ts +69 -0
  565. package/src/indexing/client.ts +1184 -0
  566. package/src/indexing/index.ts +961 -0
  567. package/src/indexing/profile.ts +771 -0
  568. package/src/indexing-store/cache.ts +1056 -0
  569. package/src/indexing-store/historical.ts +555 -0
  570. package/src/indexing-store/index.ts +73 -0
  571. package/src/indexing-store/profile.ts +557 -0
  572. package/src/indexing-store/realtime.ts +412 -0
  573. package/src/indexing-store/utils.ts +162 -0
  574. package/src/internal/common.ts +15 -0
  575. package/src/internal/errors.ts +228 -0
  576. package/src/internal/logger.ts +252 -0
  577. package/src/internal/metrics.ts +1027 -0
  578. package/src/internal/options.ts +130 -0
  579. package/src/internal/shutdown.ts +32 -0
  580. package/src/internal/telemetry.ts +303 -0
  581. package/src/internal/types.ts +598 -0
  582. package/src/rpc/actions.ts +1344 -0
  583. package/src/rpc/http.ts +164 -0
  584. package/src/rpc/index.ts +959 -0
  585. package/src/runtime/events.ts +875 -0
  586. package/src/runtime/filter.ts +664 -0
  587. package/src/runtime/fragments.ts +674 -0
  588. package/src/runtime/historical.ts +1556 -0
  589. package/src/runtime/index.ts +578 -0
  590. package/src/runtime/init.ts +49 -0
  591. package/src/runtime/isolated.ts +769 -0
  592. package/src/runtime/multichain.ts +853 -0
  593. package/src/runtime/omnichain.ts +913 -0
  594. package/src/runtime/realtime.ts +1179 -0
  595. package/src/server/error.ts +68 -0
  596. package/src/server/index.ts +173 -0
  597. package/src/sync-historical/index.ts +1062 -0
  598. package/src/sync-historical/portal-realtime-wire.ts +389 -0
  599. package/src/sync-historical/portal-realtime.ts +209 -0
  600. package/src/sync-historical/portal-transform.ts +123 -0
  601. package/src/sync-historical/portal.ts +811 -0
  602. package/src/sync-historical/realtime.ts +132 -0
  603. package/src/sync-realtime/bloom.ts +102 -0
  604. package/src/sync-realtime/index.ts +1298 -0
  605. package/src/sync-store/encode.ts +153 -0
  606. package/src/sync-store/index.ts +1633 -0
  607. package/src/sync-store/migrations.ts +1801 -0
  608. package/src/sync-store/schema.ts +248 -0
  609. package/src/types/db.ts +292 -0
  610. package/src/types/eth.ts +216 -0
  611. package/src/types/utils.ts +47 -0
  612. package/src/types/virtual.ts +244 -0
  613. package/src/types.d.ts +38 -0
  614. package/src/ui/app.ts +207 -0
  615. package/src/ui/index.ts +37 -0
  616. package/src/ui/patch.ts +145 -0
  617. package/src/utils/abi.ts +103 -0
  618. package/src/utils/bigint.ts +41 -0
  619. package/src/utils/chains.ts +22 -0
  620. package/src/utils/checkpoint.ts +203 -0
  621. package/src/utils/chunk.ts +7 -0
  622. package/src/utils/copy.ts +151 -0
  623. package/src/utils/date.ts +26 -0
  624. package/src/utils/debug.ts +110 -0
  625. package/src/utils/decodeAbiParameters.ts +428 -0
  626. package/src/utils/decodeEventLog.ts +100 -0
  627. package/src/utils/dedupe.ts +32 -0
  628. package/src/utils/duplicates.ts +19 -0
  629. package/src/utils/estimate.ts +27 -0
  630. package/src/utils/finality.ts +40 -0
  631. package/src/utils/format.ts +22 -0
  632. package/src/utils/generators.ts +157 -0
  633. package/src/utils/hash.ts +22 -0
  634. package/src/utils/interval.ts +212 -0
  635. package/src/utils/lowercase.ts +6 -0
  636. package/src/utils/mutex.ts +33 -0
  637. package/src/utils/never.ts +3 -0
  638. package/src/utils/offset.ts +133 -0
  639. package/src/utils/order.ts +16 -0
  640. package/src/utils/partition.ts +53 -0
  641. package/src/utils/pg.ts +197 -0
  642. package/src/utils/pglite.ts +97 -0
  643. package/src/utils/port.ts +34 -0
  644. package/src/utils/print.ts +31 -0
  645. package/src/utils/promiseAllSettledWithThrow.ts +27 -0
  646. package/src/utils/promiseWithResolvers.ts +20 -0
  647. package/src/utils/queue.ts +258 -0
  648. package/src/utils/range.ts +8 -0
  649. package/src/utils/result.ts +26 -0
  650. package/src/utils/sql-parse.ts +1477 -0
  651. package/src/utils/timer.ts +8 -0
  652. package/src/utils/truncate.ts +15 -0
  653. package/src/utils/wait.ts +8 -0
  654. package/src/utils/zipper.ts +80 -0
@@ -0,0 +1,811 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import type { Common } from "@/internal/common.js";
3
+ import type {
4
+ Chain,
5
+ FactoryId,
6
+ Filter,
7
+ LogFilter,
8
+ SyncBlock,
9
+ SyncBlockHeader,
10
+ SyncLog,
11
+ SyncTrace,
12
+ SyncTransaction,
13
+ SyncTransactionReceipt,
14
+ } from "@/internal/types.js";
15
+ import {
16
+ getChildAddress,
17
+ getFilterFactories,
18
+ isAddressFactory,
19
+ isAddressMatched,
20
+ isBlockFilterMatched,
21
+ isLogFactoryMatched,
22
+ isTraceFilterMatched,
23
+ isTransactionFilterMatched,
24
+ isTransferFilterMatched,
25
+ } from "@/runtime/filter.js";
26
+ import type { Rpc } from "@/rpc/index.js";
27
+ import type { Interval } from "@/utils/interval.js";
28
+ import { type Address, type Hex } from "viem";
29
+ import { type HistoricalSync, createHistoricalSync } from "./index.js";
30
+ import { type RawHeader, hx, isFinalityGap, toSyncLog, toSyncBlockHeader, toSyncTransaction, toSyncReceipt, parityToCallFrame, cmpTraceAddr, traceSafeChunkBlocks } from "./portal-transform.js";
31
+
32
+ /**
33
+ * Portal-backed historical sync with a PARALLEL read-ahead chunk buffer.
34
+ *
35
+ * Ponder feeds small intervals; Portal is latency-bound per request but has huge
36
+ * parallel bandwidth. So we fetch large aligned CHUNKS and serve every interval
37
+ * from cache — and we fetch chunks IN PARALLEL (read-ahead depth N) so the
38
+ * Portal's per-request latency overlaps instead of serializing.
39
+ *
40
+ * Correctness for factory sources: the discovery timeline is decoupled from the
41
+ * data timeline. Each chunk's children are discovered independently (clamped to
42
+ * the factory's real start block), and a data chunk only fetches once discovery
43
+ * is complete THROUGH its own block range — so no child event is missed even
44
+ * though data chunks are fetched out of order.
45
+ *
46
+ * Tunables: PORTAL_CHUNK_BLOCKS (default 500k), PORTAL_READAHEAD (default 6).
47
+ * Selected at runtime/historical.ts when `chain.portal` is set; realtime → rpc.
48
+ */
49
+
50
+ type CreateHistoricalSyncParameters = {
51
+ common: Common;
52
+ chain: Chain;
53
+ rpc: Rpc;
54
+ childAddresses: Map<FactoryId, Map<Address, number>>;
55
+ // FULL per-chain filter set (runtime: params.eventCallbacks). The fetch-spec is resolved from
56
+ // THIS, once, not from per-call requiredIntervals (which is only the subset still needed and
57
+ // shrinks as fragments cache) — so every idx-keyed chunk is filter-complete. (C1)
58
+ eventCallbacks: { filter: Filter }[];
59
+ };
60
+
61
+ type PortalLogRequest = { address?: string[]; topic0?: string[]; topic1?: string[]; topic2?: string[]; topic3?: string[]; transaction?: boolean };
62
+ type ChunkData = {
63
+ headers: Map<number, RawHeader>;
64
+ logs: Map<number, any[]>;
65
+ txs: Map<number, any[]>;
66
+ // for trace/transfer sources: full block + all its traces + its txs, by block number
67
+ traceBlocks: Map<number, { header: RawHeader; traces: any[]; txs: any[] }>;
68
+ // for block-interval sources: headers of blocks matching a BlockFilter (interval/offset)
69
+ blockHeaders: Map<number, RawHeader>;
70
+ // for account transaction sources: blocks + their from/to-matched txs, by block number
71
+ txBlocks: Map<number, { header: RawHeader; txs: any[] }>;
72
+ };
73
+
74
+ const PORTAL_MAX_ADDRESSES = 1000;
75
+ // Portal rejects any request whose raw body exceeds this (sqd-network transport/src/protocol.rs:
76
+ // `MAX_RAW_QUERY_SIZE = 256 * 1024`) with 400 "Query is too large". The body is dominated by filter
77
+ // address lists (factory children in log/tx filters). We keep under it by merging per-event log
78
+ // filters + batching addresses; a body that still overflows fails loud (see fetchBatch).
79
+ const MAX_RAW_QUERY_SIZE = 256 * 1024;
80
+ const CHUNK_BLOCKS = Number(process.env.PORTAL_CHUNK_BLOCKS ?? 500_000);
81
+ const READAHEAD = Number(process.env.PORTAL_READAHEAD ?? 6);
82
+ // The Portal fans ONE stream request out across up to `buffer_size` chunk-workers concurrently
83
+ // (default 10, clamped to 1000) at ZERO extra CU. Without it a wide/sparse scan runs ~10-wide and
84
+ // the head-of-line front chunk stalls → the stream truncates (verified: an empty [0,5M] factory scan
85
+ // terminates at 60s with the default vs completes in 17.5s at 100). Set high; the Portal's own
86
+ // download window (~500) is the real ceiling, and CU is charged per chunk touched regardless.
87
+ const BUFFER_SIZE = Number(process.env.PORTAL_BUFFER_SIZE ?? 100);
88
+ // Discovery splits [deploy, head] into this many DISJOINT windows fetched CONCURRENTLY (separate
89
+ // streams — the Portal serializes one stream in block order, so parallelism comes from disjoint
90
+ // requests). Bounded so tiny ranges aren't over-split.
91
+ const DISCOVERY_WINDOWS = Number(process.env.PORTAL_DISCOVERY_WINDOWS ?? 8);
92
+ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
93
+
94
+ // ── Global adaptive Portal controller — module scope, SHARED across every per-chain sync ──────────
95
+ // All chains stream from the SAME Portal endpoint, so request concurrency, CU/throttle headroom and
96
+ // buffered memory are ONE shared budget, not per-chain (15 chains each running a private read-ahead
97
+ // is what OOMs and gets CU-throttled). Portal mode ≠ RPC: it prefers a FEW long, data-heavy requests
98
+ // over RPC's aggressive low-latency fan-out, so the objective is not max RPS but to keep every chain's
99
+ // read-ahead buffer FULL while bounding total memory — so indexing is bottlenecked by local
100
+ // decode/write and NEVER by awaiting a fetch (the whole promise of the Portal). Two controls, both
101
+ // zero-config + self-tuning (the client can't know the endpoint's limits, which drift over time):
102
+ // • AIMD concurrency — start low, ramp one slot per clean generation, halve on 429/503/timeout.
103
+ // Discovers the endpoint's LIVE capacity and re-adapts (mirrors Ponder's native RPC AIMD,
104
+ // rpc/index.ts), but GLOBAL because the endpoint is shared.
105
+ // • Rows-in-memory budget — read-ahead prefetches until the shared buffer reaches it, then
106
+ // backpressures. Caps memory regardless of chain count; consumption drains + evicts → refills.
107
+ const portalGate = (() => {
108
+ // Concurrency must feed however many chains share the endpoint, so the floor is generous (a
109
+ // multichain app parks one in-flight request per chain plus read-ahead). Memory — not concurrency —
110
+ // is the OOM guard (the rows budget below), so a higher concurrency ceiling is safe. AIMD still
111
+ // discovers the true ceiling: ramp while clean, halve on throttle. All zero-config.
112
+ const MIN = Number(process.env.PORTAL_MIN_CONCURRENCY ?? 8);
113
+ const MAX = Number(process.env.PORTAL_MAX_CONCURRENCY ?? 48);
114
+ const START = Number(process.env.PORTAL_START_CONCURRENCY ?? 16);
115
+ // Backpressure threshold on buffered rows (log/tx/trace/block records held across all chains'
116
+ // read-ahead). A buffered record costs ~5-10 KB live in V8 once ponder's derived copies are
117
+ // counted, so 250k ≈ 1.5-2.5 GB — it must engage BEFORE the heap dies. The prior 1.2M was dead
118
+ // code: a 4 GB heap OOMs at ~450k rows, so the cap never fired. Scale up with --max-old-space-size.
119
+ const MAX_ROWS = Number(process.env.PORTAL_MAX_ROWS_IN_MEM ?? 250_000);
120
+ let limit = START, active = 0, ok = 0, rows = 0;
121
+ const waiters: (() => void)[] = [];
122
+ const pump = () => { while (active < limit && waiters.length > 0) { active++; waiters.shift()!(); } };
123
+ return {
124
+ acquire: (): Promise<void> => new Promise<void>((r) => { waiters.push(r); pump(); }),
125
+ release: () => { active = Math.max(0, active - 1); pump(); },
126
+ onOk: () => { if (++ok >= 8 && limit < MAX) { limit = Math.min(MAX, limit + 2); ok = 0; pump(); } }, // additive ramp (+2 / 8 clean)
127
+ onThrottle: () => { limit = Math.max(MIN, Math.floor(limit / 2)); ok = 0; }, // multiplicative back-off
128
+ addRows: (n: number) => { rows += n; },
129
+ freeRows: (n: number) => { rows = Math.max(0, rows - n); },
130
+ saturated: () => rows >= MAX_ROWS, // memory backpressure for read-ahead (never gates the needed chunk)
131
+ snapshot: () => ({ limit, active, rows }),
132
+ };
133
+ })();
134
+ // opt-in observability: watch the AIMD concurrency + memory backpressure adapt live.
135
+ if (process.env.PORTAL_GATE_LOG) setInterval(() => { const s = portalGate.snapshot(); console.log(`[portalGate] concurrency_limit=${s.limit} active=${s.active} buffered_rows=${s.rows}`); }, 20_000).unref();
136
+
137
+ const asArr = (t: Hex | readonly Hex[] | null | undefined): string[] | undefined => {
138
+ if (t === null || t === undefined) return undefined;
139
+ return (Array.isArray(t) ? t : [t]).map((x) => (x as string).toLowerCase());
140
+ };
141
+
142
+ export const createPortalHistoricalSync = (
143
+ args: CreateHistoricalSyncParameters,
144
+ ): HistoricalSync => {
145
+ const portalUrl = args.chain.portal!.replace(/\/$/, "");
146
+ const log = args.common.logger;
147
+ const baseHeaders: Record<string, string> = { "content-type": "application/json", "accept-encoding": "gzip" };
148
+ if (process.env.PORTAL_API_KEY) baseHeaders["x-api-key"] = process.env.PORTAL_API_KEY;
149
+
150
+ const stats = { dataChunks: 0, discChunks: 0, http: 0, logs: 0, errors: 0, retries: 0, bytes: 0, cacheHits: 0, inflight: 0, maxInflight: 0, blocks: 0, txs: 0, receipts: 0, traces: 0, rpcFallback: 0, gateWaitMs: 0, fetchMs: 0, transformMs: 0 };
151
+ const dataCache = new Map<number, Promise<ChunkData>>(); // keyed by chunk index
152
+ const chunkRows = new Map<number, number>(); // idx → buffered row count, for the global memory budget
153
+ let discoveredThrough = -1; // high-water block covered by the single wide factory-discovery scan
154
+ let discoveryP: Promise<void> = Promise.resolve(); // the (lazily extended) discovery scan promise
155
+ const stash = new Map<string, { blocks: SyncBlockHeader[]; txs: SyncTransaction[]; receipts: SyncTransactionReceipt[]; traces: { trace: SyncTrace; block: SyncBlock; transaction: SyncTransaction }[]; closest: SyncBlock | undefined }>();
156
+ const ikey = (i: Interval) => `${i[0]}-${i[1]}`;
157
+ let chunkBlocks = CHUNK_BLOCKS;
158
+ let chunkSizeP: Promise<void> | undefined;
159
+ const idxOf = (n: number) => Math.floor(n / chunkBlocks);
160
+ let discStartIdx: number | undefined; // factory deploy chunk — discovery floor (fixes from-0 scan)
161
+
162
+ // finality-gap fallback: Portal serves only finalized data, and its finalized head can
163
+ // (rarely) lag Ponder's target. Any interval reaching past Portal's head is delegated
164
+ // whole to the stock RPC historical sync. PORTAL_FINALIZED_HEAD overrides for tests/ops.
165
+ let portalHead: number | undefined = process.env.PORTAL_FINALIZED_HEAD ? Number(process.env.PORTAL_FINALIZED_HEAD) : undefined;
166
+ let rpcFallbackInstance: HistoricalSync | undefined;
167
+ const rpcFallback = (): HistoricalSync => (rpcFallbackInstance ??= createHistoricalSync(args));
168
+ const delegated = new Set<string>(); // interval keys routed to RPC
169
+ // Portal-native realtime (PORTAL_REALTIME="stream"): the recent region [portal-head → tip] is served by
170
+ // the Portal `/stream` in runtime/realtime.ts, and `clampFinalizedToPortalHead` lowers ponder's finalized
171
+ // block to the Portal head — so historical never targets past the head and this RPC finality-gap fallback
172
+ // is neither needed nor wanted (it's the single-thread stall this mode removes). Skip it here.
173
+ const STREAM_REALTIME = Boolean(args.chain.portal) && process.env.PORTAL_REALTIME === "stream";
174
+ const refreshPortalHead = async (): Promise<number | undefined> => {
175
+ if (process.env.PORTAL_FINALIZED_HEAD) return (portalHead = Number(process.env.PORTAL_FINALIZED_HEAD));
176
+ // retry: the head probe is cheap, and a valid head is load-bearing for the finality-gap decision.
177
+ // On persistent failure portalHead stays undefined → the caller treats "head unknown" conservatively.
178
+ for (let attempt = 0; attempt < 3; attempt++) {
179
+ try { const h = await fetch(`${portalUrl}/finalized-head`, { headers: baseHeaders }).then((r) => r.json()); if (typeof h?.number === "number") return (portalHead = h.number); } catch { /* retry */ }
180
+ await new Promise((r) => setTimeout(r, 200 * (attempt + 1)));
181
+ }
182
+ return portalHead; // may be a kept-prior value, or undefined if never probed successfully
183
+ };
184
+ // instrumentation: per-chain backfill metrics → PORTAL_METRICS_FILE.<chainId> (for the bench harness)
185
+ const METRICS_FILE = process.env.PORTAL_METRICS_FILE;
186
+ let startTime = 0;
187
+ const writeMetrics = () => {
188
+ if (!METRICS_FILE) return;
189
+ try {
190
+ writeFileSync(`${METRICS_FILE}.${args.chain.id}`, JSON.stringify({
191
+ chain: args.chain.name, chainId: args.chain.id, wallMs: startTime ? Date.now() - startTime : 0,
192
+ chunkBlocks, portalFinalizedHead: portalHead ?? null,
193
+ fetch: { dataChunks: stats.dataChunks, discChunks: stats.discChunks, http: stats.http, bytes: stats.bytes, errors: stats.errors, retries: stats.retries, cacheHits: stats.cacheHits, maxInflight: stats.maxInflight },
194
+ // saturation breakdown (cumulative ms across all requests of this chain): gate-wait = time
195
+ // blocked on the global concurrency budget; fetch = Portal I/O (POST+stream drain); transform
196
+ // = NDJSON→Sync* decode. DB-write time lives in Ponder (per-range log timing), not here.
197
+ timing: { gateWaitMs: Math.round(stats.gateWaitMs), fetchMs: Math.round(stats.fetchMs), transformMs: Math.round(stats.transformMs) },
198
+ portalGate: portalGate.snapshot(),
199
+ inserted: { logs: stats.logs, blocks: stats.blocks, txs: stats.txs, receipts: stats.receipts, traces: stats.traces },
200
+ rpcFallbackIntervals: stats.rpcFallback,
201
+ }));
202
+ } catch { /* best-effort */ }
203
+ };
204
+
205
+ // Scale chunk size by the chain's block density. High-block-rate chains (Arbitrum
206
+ // ~478M blocks ≈ 19× Ethereum) otherwise need 19× more 500k-block chunks = 19× more
207
+ // latency-bound round-trips. CU is charged per Portal data-chunk (data-density based),
208
+ // so larger BLOCK-chunks don't cost more CU — they just cut round-trips. PORTAL_CHUNK_FIXED=1 disables.
209
+ const ensureChunkSize = (): Promise<void> =>
210
+ (chunkSizeP ??= (async () => {
211
+ if (process.env.PORTAL_CHUNK_FIXED) return;
212
+ try {
213
+ const h = await fetch(`${portalUrl}/finalized-head`, { headers: baseHeaders }).then((r) => r.json());
214
+ if (typeof h?.number === "number") portalHead = h.number; // dedupe the probe: seed the finality head (C3)
215
+ const density = Math.max(1, Math.round((h.number as number) / 25_000_000));
216
+ chunkBlocks = Math.min(CHUNK_BLOCKS * density, 25_000_000);
217
+ log.debug({ service: "portal", msg: `Portal ${args.chain.name}: head=${h.number} → chunkBlocks=${chunkBlocks} (${density}× density)` });
218
+ } catch { /* keep default */ }
219
+ })());
220
+
221
+ // transient = retry: HTTP 503/529/429 AND network/socket errors (parallel load
222
+ // makes "other side closed" / ECONNRESET / fetch failed routine).
223
+ const isNetworkError = (err: any): boolean => {
224
+ const m = `${err?.message ?? ""} ${err?.cause?.message ?? ""} ${err?.cause?.code ?? ""}`.toLowerCase();
225
+ return /socket|closed|econnreset|fetch failed|terminated|timeout|network|epipe|und_err/.test(m) || err?.name === "AbortError";
226
+ };
227
+
228
+ // one POST+drain; returns blocks or "done" (204); throws (with .retryAfterMs on 503-class).
229
+ async function fetchBatch(body: string, cursor: number): Promise<{ blocks: { header: RawHeader; logs?: any[]; transactions?: any[]; traces?: any[] }[]; last: number } | "done"> {
230
+ // Proactive, uniform size guard — covers EVERY request type (logs/traces/txs/discovery) at the one
231
+ // POST choke point. A body over MAX_RAW_QUERY_SIZE would 400; surface it explicitly with the real
232
+ // driver instead. Euler's worst (eth: 897 children × 24 topics) is ~41KB, so this never fires here;
233
+ // it protects indexers with pathological filtered-address counts (esp. unbatched tx from/to sets).
234
+ if (body.length > MAX_RAW_QUERY_SIZE) {
235
+ const q = (() => { try { return JSON.parse(body); } catch { return {}; } })();
236
+ const nLog = (q.logs ?? []).reduce((s: number, r: any) => s + (r.address?.length ?? 0), 0);
237
+ const nTx = (q.transactions ?? []).reduce((s: number, r: any) => s + (r.from?.length ?? 0) + (r.to?.length ?? 0), 0);
238
+ throw new Error(
239
+ `Portal request body ${(body.length / 1024).toFixed(1)}KB exceeds MAX_RAW_QUERY_SIZE ${MAX_RAW_QUERY_SIZE / 1024}KB @ ${cursor}. ` +
240
+ `Filter addresses in this request: ${nLog} log + ${nTx} tx(from/to). ` +
241
+ `Log filters are already merged+batched (PORTAL_MAX_ADDRESSES=${PORTAL_MAX_ADDRESSES}); if this is a tx filter, its from/to set is too large to fit one request and cannot be safely split — narrow the filter.`,
242
+ );
243
+ }
244
+ const tAcq = Date.now(); await portalGate.acquire(); stats.gateWaitMs += Date.now() - tAcq; // gate-wait = concurrency back-pressure
245
+ const tFetch = Date.now();
246
+ stats.inflight++; stats.maxInflight = Math.max(stats.maxInflight, stats.inflight);
247
+ try {
248
+ const res = await fetch(`${portalUrl}/finalized-stream?buffer_size=${BUFFER_SIZE}`, { method: "POST", headers: baseHeaders, body });
249
+ stats.http++;
250
+ if (res.status === 204) { portalGate.onOk(); return "done"; }
251
+ // Transient, retry with back-off (never crash the app on one bad response): 429/529 = explicit
252
+ // throttle; ALL 5xx (500/502/503/504…) = gateway/proxy/server hiccups that return an HTML error
253
+ // page mid-backfill; 409 on the FINALIZED stream = a gateway "conflict" (finalized data doesn't
254
+ // reorg, so it's not the reorg JSON). Backing off on any of these keeps the AIMD honest.
255
+ if (res.status >= 500 || res.status === 429 || res.status === 409) {
256
+ await res.body?.cancel().catch(() => {});
257
+ const ra = Number(res.headers.get("retry-after"));
258
+ const e: any = new Error(`Portal ${res.status}`); e.retryAfterMs = Number.isFinite(ra) ? ra * 1000 : undefined;
259
+ portalGate.onThrottle(); // treat as congestion → halve global concurrency
260
+ throw e;
261
+ }
262
+ if (!res.ok) {
263
+ const text = (await res.text()).slice(0, 300);
264
+ // a dataset that lacks a requested column (e.g. Monad has no accessList) → the whole
265
+ // request 400s. Surface the column so stream() can drop the field and retry.
266
+ const m = res.status === 400 && text.match(/column '([a-z0-9_]+)' is not found in '([a-z_]+)'/i);
267
+ if (m) { const e: any = new Error(`Portal 400: unsupported column ${m[1]} in ${m[2]}`); e.unsupportedColumn = m[1]; e.unsupportedTable = m[2]; throw e; }
268
+ // OTHER schema shape: a dataset whose schema doesn't know the field at all → query PARSE
269
+ // error ("unknown field `accessList`, expected one of ..."). Find which fields-block we put
270
+ // it in (block/transaction/log/trace) so stream() can drop that field key and retry.
271
+ const u = res.status === 400 && text.match(/unknown field `([a-zA-Z0-9_]+)`/);
272
+ if (u && u[1]) {
273
+ const fn = u[1]; let table = "transaction";
274
+ try { const q = JSON.parse(body); for (const t of ["transaction", "block", "log", "trace"]) if (q?.fields?.[t] && q.fields[t][fn] !== undefined) { table = t; break; } } catch { /* default transaction */ }
275
+ const e: any = new Error(`Portal 400: unknown field ${fn} in ${table}`); e.unsupportedField = fn; e.unsupportedFieldTable = table; throw e;
276
+ }
277
+ // a dataset that doesn't begin at genesis (e.g. TAC starts at block 1) 400s when queried
278
+ // below its first block. Surface the start so stream() can clamp the cursor forward.
279
+ const s = res.status === 400 && text.match(/dataset starts (?:from|at) block (\d+)/i);
280
+ if (s) { const e: any = new Error(`Portal 400: dataset starts at block ${s[1]}`); e.datasetStartsAt = Number(s[1]); throw e; }
281
+ // a dense range (many child addresses × many event topics × wide chunk) can exceed the
282
+ // Portal's per-query size/work estimate → 400 "Query is too large". Signal stream() to
283
+ // bisect the block range and retry (adaptive; no client tuning).
284
+ if (res.status === 400 && /query is too large/i.test(text)) { const e: any = new Error(`Portal 400: query too large @ ${cursor}`); e.tooLarge = true; throw e; }
285
+ throw new Error(`Portal ${res.status} @ ${cursor}: ${text}`);
286
+ }
287
+ const reader = res.body!.getReader();
288
+ const dec = new TextDecoder();
289
+ let buf = "", last = cursor;
290
+ const blocks: { header: RawHeader; logs?: any[]; transactions?: any[]; traces?: any[] }[] = [];
291
+ const onLine = (line: string) => { if (!line) return; const b = JSON.parse(line); blocks.push(b); if (b.header?.number > last) last = b.header.number; };
292
+ for (;;) { const { done, value } = await reader.read(); if (done) break; stats.bytes += value.byteLength; buf += dec.decode(value, { stream: true }); let nl: number; while ((nl = buf.indexOf("\n")) >= 0) { onLine(buf.slice(0, nl)); buf = buf.slice(nl + 1); } }
293
+ buf += dec.decode(); if (buf) onLine(buf);
294
+ portalGate.onOk(); // clean full response → a generation of these ramps concurrency up
295
+ return { blocks, last };
296
+ } catch (err: any) {
297
+ if (isNetworkError(err)) portalGate.onThrottle(); // dropped/timed-out connections under load = congestion
298
+ throw err;
299
+ } finally { stats.fetchMs += Date.now() - tFetch; portalGate.release(); stats.inflight--; }
300
+ }
301
+
302
+ // Fields the TARGET dataset doesn't have (per-dataset schema varies — e.g. Monad's transactions
303
+ // have no accessList). Discovered from a "column not found" 400, then stripped from every request
304
+ // so the fork degrades gracefully instead of crashing. Keyed "<fieldsKey>.<field>".
305
+ // Portal reports a missing COLUMN in a plural TABLE; map back to the field key we requested.
306
+ const TABLE_TO_KEY: Record<string, string> = { transactions: "transaction", blocks: "block", logs: "log", traces: "trace" };
307
+ const COL_SPECIAL: Record<string, string> = { access_list_size: "accessList", access_list: "accessList" }; // portal's derived column ≠ snake(field)
308
+ const colToFieldKey = (col: string, table: string): string => {
309
+ const key = TABLE_TO_KEY[table] ?? table;
310
+ const field = COL_SPECIAL[col] ?? col.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase()); // snake_case → camelCase
311
+ return `${key}.${field}`;
312
+ };
313
+ const stripFields = (q: any, dropped: Set<string>): any => {
314
+ if (dropped.size === 0 || !q.fields) return q;
315
+ const fields = JSON.parse(JSON.stringify(q.fields));
316
+ for (const tf of dropped) { const i = tf.indexOf("."); const t = tf.slice(0, i), f = tf.slice(i + 1); if (fields[t]) delete fields[t][f]; }
317
+ return { ...q, fields };
318
+ };
319
+
320
+ // PER-STREAM (per block-range) field degradation. A dataset can lack a column on only SOME
321
+ // (e.g. old) chunks, so drops are LOCAL — a chunk that has the column keeps it. When a NEEDED
322
+ // field is missing we DON'T crash here (this range might be event-less/irrelevant); we drop it to
323
+ // fetch, and record it in `neededMissing` so the caller can crash ONLY IF the range yields matched
324
+ // data (see dataChunk). Unused nullable fields are dropped silently.
325
+ async function* stream(query: object, from: number, to: number, neededMissing?: Set<string>) {
326
+ let cursor = from;
327
+ const dropped = new Set<string>(), triedCols = new Set<string>();
328
+ while (cursor <= to) {
329
+ let attempt = 0;
330
+ let batch: Awaited<ReturnType<typeof fetchBatch>> | undefined;
331
+ while (batch === undefined) {
332
+ const body = JSON.stringify({ ...stripFields(query, dropped), fromBlock: cursor, toBlock: to });
333
+ try { batch = await fetchBatch(body, cursor); }
334
+ catch (err: any) {
335
+ if (err?.tooLarge) {
336
+ // Portal caps request BYTES (MAX_RAW_QUERY_SIZE), not range — so bisecting blocks can't
337
+ // help. mergeLogRequests already de-dups addresses across event filters; if a body still
338
+ // exceeds the cap the address batch itself is too big → fail loud with the actual lever.
339
+ throw new Error(`Portal query body exceeds MAX_RAW_QUERY_SIZE even after merging event filters — lower PORTAL_MAX_ADDRESSES (currently ${PORTAL_MAX_ADDRESSES}) to shrink the address batch. @ ${cursor}`);
340
+ }
341
+ if (err?.datasetStartsAt !== undefined) {
342
+ // dataset begins after this chunk's start (doesn't reach genesis) → skip the missing
343
+ // prefix. If the whole chunk precedes the dataset, there's nothing to fetch here.
344
+ if (err.datasetStartsAt > to) return;
345
+ if (err.datasetStartsAt > cursor) { cursor = err.datasetStartsAt; continue; }
346
+ throw err; // start ≤ cursor yet still 400 ⇒ not a below-start issue; surface it
347
+ }
348
+ // a dataset that can't serve a requested field — either the column is absent from the
349
+ // parquet ("column not found") or the schema doesn't know the field ("unknown field").
350
+ // Both are handled the same way: drop that field for this chunk and retry.
351
+ if (err?.unsupportedColumn || err?.unsupportedField) {
352
+ const tag = (err.unsupportedColumn ?? err.unsupportedField) as string; // unique id → bounds retries
353
+ if (triedCols.has(tag)) throw err; // dropping its field didn't help → real error
354
+ triedCols.add(tag);
355
+ const field = err.unsupportedColumn ? colToFieldKey(err.unsupportedColumn, err.unsupportedTable) : `${err.unsupportedFieldTable}.${err.unsupportedField}`;
356
+ dropped.add(field); // drop for THIS chunk's retries only (chunks that have it keep it)
357
+ // non-load-bearing nullable field → drop silently; anything else → crash IF matched (dataChunk).
358
+ if (!DROPPABLE_FIELDS.has(field)) neededMissing?.add(`${field} (${tag})`);
359
+ else log.debug({ service: "portal", msg: `Portal ${args.chain.name} [${from},${to}]: dataset can't serve '${tag}' → skipping non-load-bearing field ${field}` });
360
+ continue;
361
+ }
362
+ const retryable = err?.retryAfterMs !== undefined || isNetworkError(err);
363
+ if (!retryable || attempt++ >= 10) throw err;
364
+ stats.errors++; stats.retries++;
365
+ await sleep(err?.retryAfterMs !== undefined ? Math.min(err.retryAfterMs, 30_000) : Math.min(500 * 2 ** attempt, 30_000));
366
+ }
367
+ }
368
+ if (batch === "done") return;
369
+ yield batch.blocks;
370
+ if (batch.last < cursor) throw new Error(`Portal no progress @ ${cursor}`);
371
+ cursor = batch.last + 1;
372
+ }
373
+ }
374
+
375
+ function logRequestsFor(filter: LogFilter): PortalLogRequest[] {
376
+ const base: PortalLogRequest = {};
377
+ if (filter.topic0) base.topic0 = asArr(filter.topic0);
378
+ if (filter.topic1) base.topic1 = asArr(filter.topic1 as any);
379
+ if (filter.topic2) base.topic2 = asArr(filter.topic2 as any);
380
+ if (filter.topic3) base.topic3 = asArr(filter.topic3 as any);
381
+ let addresses: Address[] | undefined;
382
+ if (isAddressFactory(filter.address)) {
383
+ addresses = Array.from(args.childAddresses.get(filter.address.id)?.keys() ?? []);
384
+ if (addresses.length === 0) return [];
385
+ } else if (filter.address === undefined) return [base];
386
+ else addresses = (Array.isArray(filter.address) ? filter.address : [filter.address]).map((a) => a.toLowerCase() as Address);
387
+ const out: PortalLogRequest[] = [];
388
+ for (let i = 0; i < addresses.length; i += PORTAL_MAX_ADDRESSES) out.push({ ...base, address: addresses.slice(i, i + PORTAL_MAX_ADDRESSES) });
389
+ return out;
390
+ }
391
+
392
+ // Ponder emits ONE filter per event, so an N-event contract (e.g. the 24-event EVault) produces N
393
+ // log requests that each repeat the SAME (possibly large) child-address list with a different
394
+ // topic0. Concatenated into one query body they can exceed the Portal's raw query-size limit
395
+ // (400 "Query is too large" — it caps request BYTES, not range). Collapse requests that share the
396
+ // same address set + topic1..3 into one, unioning topic0 — identical result set, ~N× smaller body.
397
+ function mergeLogRequests(reqs: PortalLogRequest[]): PortalLogRequest[] {
398
+ const groups = new Map<string, PortalLogRequest>();
399
+ for (const r of reqs) {
400
+ const key = JSON.stringify([r.address ? [...r.address].sort() : null, r.topic1 ?? null, r.topic2 ?? null, r.topic3 ?? null]);
401
+ const g = groups.get(key);
402
+ if (!g) { groups.set(key, { ...r, topic0: r.topic0 ? [...new Set(r.topic0)] : undefined }); continue; }
403
+ if (g.topic0 === undefined || r.topic0 === undefined) g.topic0 = undefined; // one wants ALL topic0 → keep the broadest
404
+ else { const s = new Set(g.topic0); for (const t of r.topic0) s.add(t); g.topic0 = [...s]; }
405
+ }
406
+ return [...groups.values()];
407
+ }
408
+
409
+ // FILTER/PROJECTION STRATEGY (max Portal leverage): every row filter is pushed to
410
+ // Portal's native server-side filters — logs by address+topics (logRequestsFor),
411
+ // traces by callTo/callFrom/callSighash (tracePortalRequests), account txs by from/to
412
+ // (txPortalRequests). Field projection below requests exactly the columns the sync
413
+ // store persists and no more. The only client-side row filter is block-interval
414
+ // (Portal has no modulo filter), and receipt fields are added only on demand.
415
+ const REQUIRED_BLOCK_FIELDS = ["number", "hash", "parentHash", "timestamp", "logsBloom", "miner", "gasUsed", "gasLimit", "stateRoot", "receiptsRoot", "transactionsRoot", "size", "difficulty", "extraData"];
416
+ const NULLABLE_BLOCK_FIELDS = ["baseFeePerGas", "nonce", "mixHash", "sha3Uncles", "totalDifficulty"];
417
+ const LOG_FIELDS = { address: true, topics: true, data: true, transactionHash: true, transactionIndex: true, logIndex: true };
418
+ // Ponder's event profiler probes event.transaction.hash, so we pull each matched
419
+ // log's parent transaction (Portal `transaction` relation) and store it.
420
+ const TX_FIELDS = { transactionIndex: true, hash: true, from: true, to: true, input: true, value: true, nonce: true, gas: true, gasPrice: true, maxFeePerGas: true, maxPriorityFeePerGas: true, type: true, r: true, s: true, v: true, yParity: true, accessList: true };
421
+ // receipt fields ride on Portal's transaction object (no separate receipt entity)
422
+ const RECEIPT_FIELDS = { status: true, cumulativeGasUsed: true, effectiveGasPrice: true, gasUsed: true, contractAddress: true, logsBloom: true };
423
+ let needReceipts = false; // set from filters on first syncBlockRangeData (stable per chain)
424
+ // trace fields: request both flattened selectors (some Portal builds) AND rely on
425
+ // nested action/result in the response — the transform reads whichever is present.
426
+ const TRACE_FIELDS = {
427
+ transactionIndex: true, traceAddress: true, type: true, subtraces: true, error: true, revertReason: true,
428
+ callFrom: true, callTo: true, callValue: true, callGas: true, callInput: true, callSighash: true, callCallType: true, callResultGasUsed: true, callResultOutput: true,
429
+ createFrom: true, createValue: true, createGas: true, createInit: true, createResultGasUsed: true, createResultCode: true, createResultAddress: true,
430
+ suicideAddress: true, suicideRefundAddress: true, suicideBalance: true,
431
+ };
432
+ let needTraces = false;
433
+ let traceFilters: any[] = [];
434
+ let transferFilters: any[] = [];
435
+ let needBlocks = false;
436
+ let blockFilters: any[] = [];
437
+ let needTxFilter = false;
438
+ let transactionFilters: any[] = [];
439
+ let logFilters: LogFilter[] = [];
440
+ let allFactories: any[] = [];
441
+ let backfillStartBlock = 0;
442
+ let backfillEndBlock: number | undefined; // undefined ⇒ unbounded (backfill to the finalized head)
443
+ // Fields that are NULLABLE in Ponder's sync-store AND non-load-bearing — Ponder never uses them
444
+ // internally and they're legitimately absent on some chains (accessList on non-typed txs; nonce/
445
+ // mixHash on PoS; baseFeePerGas pre-1559; totalDifficulty post-merge). Safe to store as null when a
446
+ // dataset lacks them. NOTE: Ponder's per-filter `include` is a STATIC default that always lists
447
+ // EVERY standard field incl. accessList (runtime/filter.ts defaultTransactionInclude), so it can't
448
+ // tell us what a handler actually reads — we classify by field. Anything NOT here, missing ⇒ crash
449
+ // (a NOT-NULL / bloom-load-bearing / core column whose absence would corrupt or silently gut data).
450
+ const DROPPABLE_FIELDS = new Set(["transaction.accessList", "block.baseFeePerGas", "block.nonce", "block.mixHash", "block.sha3Uncles", "block.totalDifficulty"]);
451
+ // Resolve the COMPLETE chain-wide fetch-spec ONCE from args.eventCallbacks (the FULL per-chain
452
+ // filter set), NOT from per-call requiredIntervals (only the subset Ponder still needs, which
453
+ // shrinks as fragments cache). Chunks are cached by idx ALONE, so every chunk MUST be filter-
454
+ // complete — else a filter that first needs an already-cached chunk is never streamed, yet its
455
+ // interval is marked done → permanent silent gap. (C1)
456
+ let specReady = false;
457
+ const initSpec = () => {
458
+ if (specReady) return;
459
+ specReady = true;
460
+ const fs = (args.eventCallbacks ?? []).map((e) => e.filter);
461
+ logFilters = fs.filter((f) => f.type === "log") as LogFilter[];
462
+ allFactories = [...new Map(fs.flatMap(getFilterFactories).map((f: any) => [f.id, f])).values()];
463
+ needReceipts = fs.some((f) => (f as any).hasTransactionReceipt === true);
464
+ blockFilters = fs.filter((f) => f.type === "block"); needBlocks = blockFilters.length > 0;
465
+ transactionFilters = fs.filter((f) => f.type === "transaction"); needTxFilter = transactionFilters.length > 0;
466
+ traceFilters = fs.filter((f) => f.type === "trace");
467
+ transferFilters = fs.filter((f) => f.type === "transfer");
468
+ needTraces = traceFilters.length + transferFilters.length > 0;
469
+ // the chain's actual backfill window, from the filters — used to bound chunk fetches so a
470
+ // bounded backfill (or the backfill tail) never over-fetches. Fully automatic; no client tuning.
471
+ const froms = fs.map((f) => (f as any).fromBlock).filter((b) => b != null);
472
+ backfillStartBlock = froms.length ? Math.min(...froms) : 0;
473
+ const tos = fs.map((f) => (f as any).toBlock);
474
+ backfillEndBlock = tos.length && tos.every((t) => t != null) ? Math.max(...tos) : undefined;
475
+ };
476
+ // a chunk's grid-aligned [from,to] clamped to the real backfill window (end ⇒ explicit toBlock,
477
+ // else the finalized head). Bounds fetch on BOTH sides so a small/bounded range isn't widened to
478
+ // the 500k grid — the reason the diff harness needs no PORTAL_CHUNK_* tuning.
479
+ const chunkRange = (idx: number): [number, number] => {
480
+ const end = backfillEndBlock ?? portalHead ?? Number.POSITIVE_INFINITY;
481
+ return [Math.max(idx * chunkBlocks, backfillStartBlock), Math.min(idx * chunkBlocks + chunkBlocks - 1, end)];
482
+ };
483
+ const blockFieldsFor = (filters: Filter[]): Record<string, boolean> => {
484
+ const inc = new Set<string>();
485
+ for (const f of filters) for (const i of f.include ?? []) if (i.startsWith("block.")) inc.add(i.slice(6));
486
+ const fields: Record<string, boolean> = {};
487
+ for (const k of REQUIRED_BLOCK_FIELDS) fields[k] = true;
488
+ // always fetch the nullable header fields too — they're cheap and keep stored blocks
489
+ // byte-identical with the RPC path (which always has nonce/mixHash/sha3Uncles/totalDifficulty).
490
+ for (const k of NULLABLE_BLOCK_FIELDS) fields[k] = true;
491
+ void inc;
492
+ return fields;
493
+ };
494
+
495
+ // ---- discovery: wide factory scan over [factoryStart, head], split into PARALLEL disjoint windows ----
496
+ // A factory scan can't be pruned (logs are block-ordered, the address is scattered), so its cost is
497
+ // ~the log volume of the range and irreducible — but fully parallelizable. The Portal serializes ONE
498
+ // stream in block order (a slow front chunk truncates it), so parallelism comes from issuing DISJOINT
499
+ // windows concurrently; each stream additionally fans out `buffer_size` chunk-workers. A single
500
+ // sequential [0,head] scan was the slow start; N concurrent windows divide the wall-clock by N.
501
+ function ensureDiscoveredThrough(idx: number, factories: any[]): Promise<unknown> {
502
+ if (factories.length === 0 || discStartIdx === undefined) return Promise.resolve();
503
+ const need = chunkRange(idx)[1];
504
+ if (need <= discoveredThrough) return discoveryP; // already scanned this far
505
+ const from = discoveredThrough < 0 ? discStartIdx * chunkBlocks : discoveredThrough + 1;
506
+ const to = Math.max(need, backfillEndBlock ?? portalHead ?? need); // reach as far as the backfill will need — usually the whole span at once
507
+ discoveredThrough = to;
508
+ const earlier = discoveryP;
509
+ discoveryP = (async () => {
510
+ await earlier; // serialize extensions so children accumulate deterministically
511
+ const span = to - from + 1;
512
+ const P = Math.max(1, Math.min(DISCOVERY_WINDOWS, Math.ceil(span / chunkBlocks)));
513
+ const w = Math.ceil(span / P);
514
+ const windows: [number, number][] = [];
515
+ for (let i = 0; i < P; i++) { const lo = from + i * w; if (lo > to) break; windows.push([lo, Math.min(to, lo + w - 1)]); }
516
+ stats.discChunks += windows.length;
517
+ await Promise.all(windows.map(async ([lo, hi]) => {
518
+ for (const factory of factories) {
519
+ const needsData = factory.childAddressLocation.startsWith("offset");
520
+ const q = { type: "evm", fields: { block: { number: true }, log: { address: true, topics: true, data: needsData } }, logs: [{ address: factory.address ? (Array.isArray(factory.address) ? factory.address : [factory.address]).map((addr: string) => addr.toLowerCase()) : undefined, topic0: [factory.eventSelector.toLowerCase()] }] };
521
+ const rec = args.childAddresses.get(factory.id)!;
522
+ for await (const blocks of stream(q, lo, hi)) {
523
+ for (const bl of blocks) for (const raw of bl.logs ?? []) {
524
+ const sl = { address: (raw.address as string)?.toLowerCase(), topics: raw.topics ?? [], data: raw.data ?? "0x", blockNumber: hx(bl.header.number) } as unknown as SyncLog;
525
+ if (isLogFactoryMatched({ factory, log: sl })) {
526
+ const child = getChildAddress({ log: sl, factory }).toLowerCase() as Address;
527
+ const bn = bl.header.number; const prevBn = rec.get(child);
528
+ if (prevBn === undefined || prevBn > bn) rec.set(child, bn);
529
+ }
530
+ }
531
+ }
532
+ }
533
+ }));
534
+ })();
535
+ return discoveryP;
536
+ }
537
+
538
+ // ---- data chunk: gated on discovery-through-this-chunk, then ONE big data stream ----
539
+ function dataChunk(idx: number, factories: any[], filters: LogFilter[]): Promise<ChunkData> {
540
+ let p = dataCache.get(idx);
541
+ if (p) { stats.cacheHits++; return p; }
542
+ p = (async () => {
543
+ await ensureDiscoveredThrough(idx, factories); // correctness: children ≤ this chunk are known
544
+ stats.dataChunks++;
545
+ const [from, to] = chunkRange(idx);
546
+ const logRequests = mergeLogRequests(filters.flatMap((f) => logRequestsFor(f))).map((r) => ({ ...r, transaction: true }));
547
+ const data: ChunkData = { headers: new Map(), logs: new Map(), txs: new Map(), traceBlocks: new Map(), blockHeaders: new Map(), txBlocks: new Map() };
548
+ const neededMissing = new Set<string>(); // needed fields the dataset lacked on THIS chunk
549
+ if (logRequests.length > 0) {
550
+ const q = { type: "evm", fields: { block: blockFieldsFor(filters), log: LOG_FIELDS, transaction: needReceipts ? { ...TX_FIELDS, ...RECEIPT_FIELDS } : TX_FIELDS }, logs: logRequests };
551
+ for await (const blocks of stream(q, from, to, neededMissing)) {
552
+ for (const b of blocks) if (b.logs?.length) {
553
+ data.headers.set(b.header.number, b.header);
554
+ data.logs.set(b.header.number, (data.logs.get(b.header.number) ?? []).concat(b.logs));
555
+ if (b.transactions?.length) data.txs.set(b.header.number, (data.txs.get(b.header.number) ?? []).concat(b.transactions));
556
+ stats.logs += b.logs.length;
557
+ }
558
+ }
559
+ }
560
+ if (needTraces) {
561
+ const tq = { type: "evm", fields: { block: blockFieldsFor(filters), trace: TRACE_FIELDS, transaction: needReceipts ? { ...TX_FIELDS, ...RECEIPT_FIELDS } : TX_FIELDS }, traces: tracePortalRequests() };
562
+ for await (const blocks of stream(tq, from, to, neededMissing)) {
563
+ for (const b of blocks) if (b.traces?.length) {
564
+ const ex = data.traceBlocks.get(b.header.number);
565
+ if (ex) { ex.traces.push(...b.traces); if (b.transactions) ex.txs.push(...b.transactions); }
566
+ else data.traceBlocks.set(b.header.number, { header: b.header, traces: b.traces, txs: b.transactions ?? [] });
567
+ }
568
+ }
569
+ }
570
+ // block-interval sources: includeAllBlocks range-scan (Portal has no modulo filter),
571
+ // keep only headers matching a BlockFilter's interval/offset.
572
+ if (needBlocks) {
573
+ const bq = { type: "evm", includeAllBlocks: true, fields: { block: blockFieldsFor(blockFilters) } };
574
+ for await (const blocks of stream(bq, from, to, neededMissing)) {
575
+ for (const b of blocks) {
576
+ const bn = b.header.number;
577
+ if (blockFilters.some((f) => isBlockFilterMatched({ filter: f, block: { number: BigInt(bn) } }))) data.blockHeaders.set(bn, b.header);
578
+ }
579
+ }
580
+ }
581
+ // account transaction sources: Portal transactions[] from/to filter pushed server-side
582
+ if (needTxFilter) {
583
+ const txReqs = txPortalRequests();
584
+ if (txReqs.length) {
585
+ const tq = { type: "evm", fields: { block: blockFieldsFor(transactionFilters), transaction: needReceipts ? { ...TX_FIELDS, ...RECEIPT_FIELDS } : TX_FIELDS }, transactions: txReqs };
586
+ for await (const blocks of stream(tq, from, to, neededMissing)) {
587
+ for (const b of blocks) if (b.transactions?.length) {
588
+ const ex = data.txBlocks.get(b.header.number);
589
+ if (ex) ex.txs.push(...b.transactions);
590
+ else data.txBlocks.set(b.header.number, { header: b.header, txs: b.transactions });
591
+ }
592
+ }
593
+ }
594
+ }
595
+ // The dataset lacked a NEEDED field on THIS chunk. Crash ONLY IF the chunk yielded MATCHED
596
+ // data — an event the indexer processes would be incomplete. If the chunk is event-less
597
+ // (old/irrelevant range), the gap is harmless, so proceed. (silent bug ≫ crash, but only
598
+ // when it actually affects the indexer's data.)
599
+ if (neededMissing.size && (data.logs.size || data.traceBlocks.size || data.txBlocks.size || data.blockHeaders.size)) {
600
+ throw new Error(`Portal dataset for ${args.chain.name} is missing [${[...neededMissing].join(", ")}] on blocks [${from},${to}], which contain matched data your indexer needs — a Portal dataset-completeness gap. Failing fast rather than serving incomplete data; report the gap to SQD, or start your indexer past the affected range.`);
601
+ }
602
+ // register this chunk's buffered size with the GLOBAL memory budget (freed when evicted).
603
+ let rc = data.blockHeaders.size;
604
+ for (const a of data.logs.values()) rc += a.length;
605
+ for (const a of data.txs.values()) rc += a.length;
606
+ for (const b of data.traceBlocks.values()) rc += b.traces.length + b.txs.length;
607
+ for (const b of data.txBlocks.values()) rc += b.txs.length;
608
+ chunkRows.set(idx, rc); portalGate.addRows(rc);
609
+ return data;
610
+ })();
611
+ dataCache.set(idx, p);
612
+ return p;
613
+ }
614
+
615
+
616
+ const factoryAddrOk = (filterAddr: any, addr: string | undefined, bn: number): boolean =>
617
+ !isAddressFactory(filterAddr) || isAddressMatched({ address: addr as Address, blockNumber: bn, childAddresses: args.childAddresses.get(filterAddr.id)! });
618
+ const traceMatched = (frame: any, bn: number): boolean => {
619
+ const blk = { number: BigInt(bn) } as any;
620
+ for (const f of transferFilters) if (isTransferFilterMatched({ filter: f, trace: frame, block: blk }) && factoryAddrOk(f.fromAddress, frame.from, bn) && factoryAddrOk(f.toAddress, frame.to, bn)) return true;
621
+ for (const f of traceFilters) if (isTraceFilterMatched({ filter: f, trace: frame, block: blk }) && factoryAddrOk(f.fromAddress, frame.from, bn) && factoryAddrOk(f.toAddress, frame.to, bn)) return true;
622
+ return false;
623
+ };
624
+ const buildTraces = (cd: ChunkData, lo: number, hi: number): { trace: SyncTrace; block: SyncBlock; transaction: SyncTransaction }[] => {
625
+ const out: { trace: SyncTrace; block: SyncBlock; transaction: SyncTransaction }[] = [];
626
+ for (const [bn, tb] of cd.traceBlocks) {
627
+ if (bn < lo || bn > hi || !tb.traces?.length) continue;
628
+ const block = toSyncBlockHeader(tb.header) as unknown as SyncBlock; // encodeTrace only reads block.number
629
+ const txByIdx = new Map<number, any>();
630
+ for (const tx of tb.txs ?? []) txByIdx.set(tx.transactionIndex, tx);
631
+ const byTx = new Map<number, any[]>();
632
+ // callTracer has no block-reward frames; skip reward/no-tx traces so `?? 0` can't fold them
633
+ // into tx 0 and shift its DFS ranks (now that we fetch the full, unfiltered trace set).
634
+ for (const t of tb.traces) { if (t.transactionIndex == null || t.type === "reward") continue; const k = t.transactionIndex; if (!byTx.has(k)) byTx.set(k, []); byTx.get(k)!.push(t); }
635
+ for (const [txIndex, traces] of byTx) {
636
+ traces.sort((x, y) => cmpTraceAddr(x.traceAddress ?? [], y.traceAddress ?? []));
637
+ const rawTx = txByIdx.get(txIndex);
638
+ traces.forEach((t, i) => {
639
+ const frame = parityToCallFrame(t, i);
640
+ if (!frame || !traceMatched(frame, bn)) return;
641
+ out.push({ trace: { trace: frame, transactionHash: rawTx?.hash } as unknown as SyncTrace, block, transaction: rawTx ? toSyncTransaction(rawTx, tb.header) : ({ transactionIndex: hx(txIndex) } as unknown as SyncTransaction) });
642
+ });
643
+ }
644
+ }
645
+ return out;
646
+ };
647
+ // Trace-index parity with the RPC path: Ponder assigns `trace_index` as the PRE-ORDER DFS rank
648
+ // over each tx's FULL call tree (rpc/actions.ts dfs(), which numbers EVERY frame, THEN filters —
649
+ // so a matched trace keeps its full-tree position). Pushing callTo/callFrom/callSighash would make
650
+ // Portal return only the matched SUBSET, so buildTraces' per-tx rank would be filter-local (a lone
651
+ // deep match → 0) instead of its true position (e.g. 7). So fetch EVERY trace and let buildTraces
652
+ // client-filter (traceMatched) AFTER ranking. Covers trace AND transfer sources. The cost is real
653
+ // (no server-side trace filter → ~all traces of the chunk); bounded by PORTAL_TRACE_CHUNK_BLOCKS.
654
+ const tracePortalRequests = (): any[] => [{ transaction: true }];
655
+ // push account TransactionFilters (from/to) to Portal's transactions[] (server-side row filter)
656
+ const txPortalRequests = (): any[] => {
657
+ const reqs: any[] = [];
658
+ const addrsOf = (a: any): string[] | undefined => {
659
+ if (a === undefined) return undefined;
660
+ if (isAddressFactory(a)) return Array.from(args.childAddresses.get(a.id)?.keys() ?? []);
661
+ return (Array.isArray(a) ? a : [a]).map((x: string) => x.toLowerCase());
662
+ };
663
+ for (const f of transactionFilters) {
664
+ const req: any = {};
665
+ const from = addrsOf(f.fromAddress); if (from?.length) req.from = from;
666
+ const to = addrsOf(f.toAddress); if (to?.length) req.to = to;
667
+ if (req.from || req.to) reqs.push(req); // skip match-all (never fetch every tx)
668
+ }
669
+ return reqs;
670
+ };
671
+
672
+ return {
673
+ async syncBlockRangeData(params) {
674
+ const { interval, requiredFactoryIntervals, syncStore } = params;
675
+ if (!startTime) startTime = Date.now();
676
+ // finality gap: if this interval reaches past Portal's finalized head, re-confirm
677
+ // (Portal advances) and, if still beyond, delegate the whole interval to RPC.
678
+ if (portalHead === undefined) await refreshPortalHead();
679
+ // C3: head UNKNOWN (probe persistently failing) OR interval past the head → don't risk silently
680
+ // under-serving the tip from Portal (it would 204 the missing tail and mark it synced).
681
+ // Re-confirm (Portal advances), then delegate the whole interval to the authoritative RPC.
682
+ if (portalHead === undefined || isFinalityGap(interval[1], portalHead)) {
683
+ await refreshPortalHead();
684
+ if (portalHead === undefined || isFinalityGap(interval[1], portalHead)) {
685
+ // Stream-realtime mode: do NOT delegate to RPC — the Portal `/stream` covers [portal-head → tip].
686
+ // With clampFinalizedToPortalHead this branch is unreachable (finalized ≤ portal-head), so it only
687
+ // fires if the head probe is failing (portalHead === undefined) — a loud degradation, not silent.
688
+ if (STREAM_REALTIME) {
689
+ log.warn({ service: "portal", msg: `Portal ${args.chain.name} [${interval[0]},${interval[1]}] past/unknown finalized head in stream mode → RPC fallback suppressed (realtime /stream covers the gap)` });
690
+ return [];
691
+ }
692
+ delegated.add(ikey(interval)); stats.rpcFallback++;
693
+ log.debug({ service: "portal", msg: `Portal ${args.chain.name} [${interval[0]},${interval[1]}] ${portalHead === undefined ? "head unknown" : `past finalized head ${portalHead}`} → RPC fallback` });
694
+ return rpcFallback().syncBlockRangeData(params);
695
+ }
696
+ }
697
+ await ensureChunkSize(); // scale chunk to chain block-density before any idxOf()
698
+ initSpec(); // freeze the COMPLETE filter/factory set once → every cached chunk is filter-complete (C1)
699
+ const filters = logFilters;
700
+ const factories = allFactories;
701
+ // cap the chunk grid BEFORE any idxOf() for DENSE sources (traces fetch every trace;
702
+ // block sources includeAllBlocks-scan the WHOLE chunk range) — bounds memory + overfetch.
703
+ const capped = traceSafeChunkBlocks(chunkBlocks, needTraces || needBlocks);
704
+ if (capped !== chunkBlocks) {
705
+ chunkBlocks = capped; dataCache.clear(); for (const r of chunkRows.values()) portalGate.freeRows(r); chunkRows.clear(); discStartIdx = undefined; discoveredThrough = -1; discoveryP = Promise.resolve();
706
+ log.debug({ service: "portal", msg: `Portal ${args.chain.name}: dense sources → chunkBlocks capped to ${chunkBlocks} (grid reset)` });
707
+ }
708
+
709
+ // pin the discovery floor at the factory's real start (NOT block 0), after any chunk cap.
710
+ // C4: clamp DOWNWARD only — a later call whose required factory interval starts earlier must
711
+ // LOWER the floor, never stay latched too high (which would skip early child discovery).
712
+ if (requiredFactoryIntervals.length > 0) {
713
+ const floor = idxOf(Math.min(...requiredFactoryIntervals.map((r) => r.interval[0]).concat(interval[0])));
714
+ discStartIdx = discStartIdx === undefined ? floor : Math.min(discStartIdx, floor);
715
+ }
716
+
717
+ const startIdx = idxOf(interval[0]), endIdx = idxOf(interval[1]);
718
+ const idxs: number[] = [];
719
+ for (let i = startIdx; i <= endIdx; i++) idxs.push(i);
720
+ const data = await Promise.all(idxs.map((i) => dataChunk(i, factories, filters)));
721
+ // PARALLEL read-ahead: prefetch the next chunks concurrently — but never past the backfill end
722
+ // (bounded toBlock or finalized head), so the tail doesn't waste CU / hit 204s. Depth is bounded
723
+ // by the GLOBAL memory budget, not a fixed count: always prefetch lead-1 (so this chain's next
724
+ // chunk is ready and indexing never awaits a fetch), and go deeper only while the shared buffer
725
+ // isn't saturated — so a fast Portal keeps every chain fed while total memory stays capped.
726
+ const raEnd = backfillEndBlock ?? portalHead ?? Number.POSITIVE_INFINITY;
727
+ for (let d = 1; d <= READAHEAD; d++) { if ((endIdx + d) * chunkBlocks > raEnd) break; if (d > 1 && portalGate.saturated()) break; void dataChunk(endIdx + d, factories, filters).catch(() => {}); }
728
+
729
+ const tXform = Date.now(); // decode/transform time: Portal NDJSON → Ponder Sync* shapes
730
+ const syncLogs: SyncLog[] = [];
731
+ const blocksByNumber = new Map<number, SyncBlockHeader>();
732
+ const syncTxs: SyncTransaction[] = [];
733
+ const syncReceipts: SyncTransactionReceipt[] = [];
734
+ const seenTx = new Set<string>();
735
+ for (const cd of data) for (const [bn, hdr] of cd.headers) {
736
+ if (bn < interval[0] || bn > interval[1]) continue;
737
+ const logs = cd.logs.get(bn) ?? [];
738
+ if (logs.length) {
739
+ blocksByNumber.set(bn, toSyncBlockHeader(hdr));
740
+ for (const raw of logs) syncLogs.push(toSyncLog(raw, hdr));
741
+ for (const tx of cd.txs.get(bn) ?? []) if (!seenTx.has(tx.hash)) {
742
+ seenTx.add(tx.hash);
743
+ syncTxs.push(toSyncTransaction(tx, hdr));
744
+ if (needReceipts) syncReceipts.push(toSyncReceipt(tx, hdr));
745
+ }
746
+ }
747
+ }
748
+ // block-interval sources: ensure each matched block is in the blocks table
749
+ if (needBlocks) for (const cd of data) for (const [bn, hdr] of cd.blockHeaders) {
750
+ if (bn >= interval[0] && bn <= interval[1] && !blocksByNumber.has(bn)) blocksByNumber.set(bn, toSyncBlockHeader(hdr));
751
+ }
752
+ // account transaction sources: re-match Portal's from/to-filtered txs (+ factory + range), insert tx/receipt/block
753
+ if (needTxFilter) for (const cd of data) for (const [bn, tb] of cd.txBlocks) {
754
+ if (bn < interval[0] || bn > interval[1]) continue;
755
+ for (const raw of tb.txs) {
756
+ if (seenTx.has(raw.hash)) continue;
757
+ const tx = toSyncTransaction(raw, tb.header);
758
+ if (!transactionFilters.some((f) => isTransactionFilterMatched({ filter: f, transaction: tx }) && factoryAddrOk(f.fromAddress, tx.from, bn) && factoryAddrOk(f.toAddress, (tx.to ?? undefined) as any, bn))) continue;
759
+ seenTx.add(raw.hash);
760
+ blocksByNumber.set(bn, toSyncBlockHeader(tb.header));
761
+ syncTxs.push(tx);
762
+ if (needReceipts) syncReceipts.push(toSyncReceipt(raw, tb.header));
763
+ }
764
+ }
765
+ for (const i of dataCache.keys()) if ((i + 1) * chunkBlocks <= interval[0]) { dataCache.delete(i); portalGate.freeRows(chunkRows.get(i) ?? 0); chunkRows.delete(i); } // evict behind + free its memory budget
766
+
767
+ const syncTraces = needTraces ? data.flatMap((cd) => buildTraces(cd, interval[0], interval[1])) : [];
768
+ stats.transformMs += Date.now() - tXform;
769
+
770
+ // C9: highest block with data — a loop, NOT Math.max(...spread) which RangeErrors on ~100k+
771
+ // keys — and INCLUDING trace-only blocks (a block with only matched traces isn't in
772
+ // blocksByNumber) so `closest` doesn't understate the synced tip.
773
+ let closest: SyncBlock | undefined;
774
+ let maxBn = -1;
775
+ for (const [bn, hdr] of blocksByNumber) if (bn > maxBn) { maxBn = bn; closest = hdr as unknown as SyncBlock; }
776
+ for (const t of syncTraces) { const bn = Number((t.block as any).number); if (bn > maxBn) { maxBn = bn; closest = t.block; } }
777
+ await syncStore.insertLogs({ logs: syncLogs, chainId: args.chain.id });
778
+ stash.set(ikey(interval), { blocks: [...blocksByNumber.values()], txs: syncTxs, receipts: syncReceipts, traces: syncTraces, closest });
779
+
780
+ log.debug({ service: "portal", msg: `Portal ${args.chain.name} [${interval[0]},${interval[1]}]: ${syncLogs.length} logs (dataChunks=${stats.dataChunks} discChunks=${stats.discChunks} http=${stats.http} hits=${stats.cacheHits} inflight=${stats.maxInflight} err=${stats.errors})` });
781
+ return syncLogs;
782
+ },
783
+
784
+ async syncBlockData(params) {
785
+ const { interval, syncStore } = params;
786
+ if (delegated.has(ikey(interval))) { delegated.delete(ikey(interval)); return rpcFallback().syncBlockData(params); }
787
+ const s = stash.get(ikey(interval));
788
+ stash.delete(ikey(interval));
789
+ if (!s) return undefined;
790
+ const chainId = args.chain.id;
791
+ // merge log blocks/txs with trace blocks/txs (a trace-only block isn't in the log set)
792
+ const blocks = new Map<string, SyncBlockHeader>();
793
+ for (const b of s.blocks) blocks.set(b.number as unknown as string, b);
794
+ const txs = new Map<string, SyncTransaction>();
795
+ for (const t of s.txs) txs.set(t.hash as unknown as string, t);
796
+ for (const { block, transaction } of s.traces) {
797
+ blocks.set((block as any).number, block as unknown as SyncBlockHeader);
798
+ if ((transaction as any)?.hash) txs.set((transaction as any).hash, transaction);
799
+ }
800
+ const blockArr = [...blocks.values()];
801
+ if (blockArr.length === 0) return s.closest;
802
+ await syncStore.insertBlocks({ blocks: blockArr, chainId });
803
+ if (txs.size) await syncStore.insertTransactions({ transactions: [...txs.values()], chainId });
804
+ if (s.receipts.length) await syncStore.insertTransactionReceipts({ transactionReceipts: s.receipts, chainId });
805
+ if (s.traces.length) await syncStore.insertTraces({ traces: s.traces, chainId });
806
+ stats.blocks += blockArr.length; stats.txs += txs.size; stats.receipts += s.receipts.length; stats.traces += s.traces.length;
807
+ writeMetrics();
808
+ return s.closest;
809
+ },
810
+ };
811
+ };