@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.
- package/CHANGELOG.md +3386 -0
- package/README.md +186 -0
- package/dist/esm/bin/commands/codegen.js +46 -0
- package/dist/esm/bin/commands/codegen.js.map +1 -0
- package/dist/esm/bin/commands/createViews.js +196 -0
- package/dist/esm/bin/commands/createViews.js.map +1 -0
- package/dist/esm/bin/commands/dev.js +430 -0
- package/dist/esm/bin/commands/dev.js.map +1 -0
- package/dist/esm/bin/commands/list.js +148 -0
- package/dist/esm/bin/commands/list.js.map +1 -0
- package/dist/esm/bin/commands/prune.js +224 -0
- package/dist/esm/bin/commands/prune.js.map +1 -0
- package/dist/esm/bin/commands/serve.js +198 -0
- package/dist/esm/bin/commands/serve.js.map +1 -0
- package/dist/esm/bin/commands/start.js +253 -0
- package/dist/esm/bin/commands/start.js.map +1 -0
- package/dist/esm/bin/isolatedController.js +200 -0
- package/dist/esm/bin/isolatedController.js.map +1 -0
- package/dist/esm/bin/isolatedWorker.js +146 -0
- package/dist/esm/bin/isolatedWorker.js.map +1 -0
- package/dist/esm/bin/ponder.js +137 -0
- package/dist/esm/bin/ponder.js.map +1 -0
- package/dist/esm/bin/utils/codegen.js +25 -0
- package/dist/esm/bin/utils/codegen.js.map +1 -0
- package/dist/esm/bin/utils/exit.js +100 -0
- package/dist/esm/bin/utils/exit.js.map +1 -0
- package/dist/esm/build/config.js +743 -0
- package/dist/esm/build/config.js.map +1 -0
- package/dist/esm/build/factory.js +76 -0
- package/dist/esm/build/factory.js.map +1 -0
- package/dist/esm/build/index.js +538 -0
- package/dist/esm/build/index.js.map +1 -0
- package/dist/esm/build/plugin.js +53 -0
- package/dist/esm/build/plugin.js.map +1 -0
- package/dist/esm/build/pre.js +76 -0
- package/dist/esm/build/pre.js.map +1 -0
- package/dist/esm/build/schema.js +164 -0
- package/dist/esm/build/schema.js.map +1 -0
- package/dist/esm/build/stacktrace.js +137 -0
- package/dist/esm/build/stacktrace.js.map +1 -0
- package/dist/esm/client/index.js +441 -0
- package/dist/esm/client/index.js.map +1 -0
- package/dist/esm/config/address.js +2 -0
- package/dist/esm/config/address.js.map +1 -0
- package/dist/esm/config/eventFilter.js +2 -0
- package/dist/esm/config/eventFilter.js.map +1 -0
- package/dist/esm/config/index.js +2 -0
- package/dist/esm/config/index.js.map +1 -0
- package/dist/esm/config/utilityTypes.js +2 -0
- package/dist/esm/config/utilityTypes.js.map +1 -0
- package/dist/esm/database/actions.js +445 -0
- package/dist/esm/database/actions.js.map +1 -0
- package/dist/esm/database/index.js +597 -0
- package/dist/esm/database/index.js.map +1 -0
- package/dist/esm/database/queryBuilder.js +310 -0
- package/dist/esm/database/queryBuilder.js.map +1 -0
- package/dist/esm/drizzle/bigint.js +38 -0
- package/dist/esm/drizzle/bigint.js.map +1 -0
- package/dist/esm/drizzle/bytes.js +47 -0
- package/dist/esm/drizzle/bytes.js.map +1 -0
- package/dist/esm/drizzle/hex.js +40 -0
- package/dist/esm/drizzle/hex.js.map +1 -0
- package/dist/esm/drizzle/index.js +40 -0
- package/dist/esm/drizzle/index.js.map +1 -0
- package/dist/esm/drizzle/json.js +119 -0
- package/dist/esm/drizzle/json.js.map +1 -0
- package/dist/esm/drizzle/kit/index.js +928 -0
- package/dist/esm/drizzle/kit/index.js.map +1 -0
- package/dist/esm/drizzle/onchain.js +158 -0
- package/dist/esm/drizzle/onchain.js.map +1 -0
- package/dist/esm/drizzle/text.js +61 -0
- package/dist/esm/drizzle/text.js.map +1 -0
- package/dist/esm/graphql/graphiql.html.js +59 -0
- package/dist/esm/graphql/graphiql.html.js.map +1 -0
- package/dist/esm/graphql/index.js +916 -0
- package/dist/esm/graphql/index.js.map +1 -0
- package/dist/esm/graphql/json.js +42 -0
- package/dist/esm/graphql/json.js.map +1 -0
- package/dist/esm/graphql/middleware.js +78 -0
- package/dist/esm/graphql/middleware.js.map +1 -0
- package/dist/esm/index.js +9 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/indexing/addStackTrace.js +54 -0
- package/dist/esm/indexing/addStackTrace.js.map +1 -0
- package/dist/esm/indexing/client.js +675 -0
- package/dist/esm/indexing/client.js.map +1 -0
- package/dist/esm/indexing/index.js +652 -0
- package/dist/esm/indexing/index.js.map +1 -0
- package/dist/esm/indexing/profile.js +584 -0
- package/dist/esm/indexing/profile.js.map +1 -0
- package/dist/esm/indexing-store/cache.js +665 -0
- package/dist/esm/indexing-store/cache.js.map +1 -0
- package/dist/esm/indexing-store/historical.js +427 -0
- package/dist/esm/indexing-store/historical.js.map +1 -0
- package/dist/esm/indexing-store/index.js +35 -0
- package/dist/esm/indexing-store/index.js.map +1 -0
- package/dist/esm/indexing-store/profile.js +428 -0
- package/dist/esm/indexing-store/profile.js.map +1 -0
- package/dist/esm/indexing-store/realtime.js +305 -0
- package/dist/esm/indexing-store/realtime.js.map +1 -0
- package/dist/esm/indexing-store/utils.js +111 -0
- package/dist/esm/indexing-store/utils.js.map +1 -0
- package/dist/esm/internal/common.js +2 -0
- package/dist/esm/internal/common.js.map +1 -0
- package/dist/esm/internal/errors.js +300 -0
- package/dist/esm/internal/errors.js.map +1 -0
- package/dist/esm/internal/logger.js +178 -0
- package/dist/esm/internal/logger.js.map +1 -0
- package/dist/esm/internal/metrics.js +1046 -0
- package/dist/esm/internal/metrics.js.map +1 -0
- package/dist/esm/internal/options.js +73 -0
- package/dist/esm/internal/options.js.map +1 -0
- package/dist/esm/internal/shutdown.js +24 -0
- package/dist/esm/internal/shutdown.js.map +1 -0
- package/dist/esm/internal/telemetry.js +200 -0
- package/dist/esm/internal/telemetry.js.map +1 -0
- package/dist/esm/internal/types.js +2 -0
- package/dist/esm/internal/types.js.map +1 -0
- package/dist/esm/rpc/actions.js +988 -0
- package/dist/esm/rpc/actions.js.map +1 -0
- package/dist/esm/rpc/http.js +130 -0
- package/dist/esm/rpc/http.js.map +1 -0
- package/dist/esm/rpc/index.js +749 -0
- package/dist/esm/rpc/index.js.map +1 -0
- package/dist/esm/runtime/events.js +664 -0
- package/dist/esm/runtime/events.js.map +1 -0
- package/dist/esm/runtime/filter.js +443 -0
- package/dist/esm/runtime/filter.js.map +1 -0
- package/dist/esm/runtime/fragments.js +478 -0
- package/dist/esm/runtime/fragments.js.map +1 -0
- package/dist/esm/runtime/historical.js +985 -0
- package/dist/esm/runtime/historical.js.map +1 -0
- package/dist/esm/runtime/index.js +325 -0
- package/dist/esm/runtime/index.js.map +1 -0
- package/dist/esm/runtime/init.js +12 -0
- package/dist/esm/runtime/init.js.map +1 -0
- package/dist/esm/runtime/isolated.js +463 -0
- package/dist/esm/runtime/isolated.js.map +1 -0
- package/dist/esm/runtime/multichain.js +509 -0
- package/dist/esm/runtime/multichain.js.map +1 -0
- package/dist/esm/runtime/omnichain.js +544 -0
- package/dist/esm/runtime/omnichain.js.map +1 -0
- package/dist/esm/runtime/realtime.js +733 -0
- package/dist/esm/runtime/realtime.js.map +1 -0
- package/dist/esm/server/error.js +56 -0
- package/dist/esm/server/error.js.map +1 -0
- package/dist/esm/server/index.js +121 -0
- package/dist/esm/server/index.js.map +1 -0
- package/dist/esm/sync-historical/index.js +701 -0
- package/dist/esm/sync-historical/index.js.map +1 -0
- package/dist/esm/sync-historical/portal-realtime-wire.js +302 -0
- package/dist/esm/sync-historical/portal-realtime-wire.js.map +1 -0
- package/dist/esm/sync-historical/portal-realtime.js +154 -0
- package/dist/esm/sync-historical/portal-realtime.js.map +1 -0
- package/dist/esm/sync-historical/portal-transform.js +113 -0
- package/dist/esm/sync-historical/portal-transform.js.map +1 -0
- package/dist/esm/sync-historical/portal.js +949 -0
- package/dist/esm/sync-historical/portal.js.map +1 -0
- package/dist/esm/sync-historical/realtime.js +127 -0
- package/dist/esm/sync-historical/realtime.js.map +1 -0
- package/dist/esm/sync-realtime/bloom.js +76 -0
- package/dist/esm/sync-realtime/bloom.js.map +1 -0
- package/dist/esm/sync-realtime/index.js +917 -0
- package/dist/esm/sync-realtime/index.js.map +1 -0
- package/dist/esm/sync-store/encode.js +105 -0
- package/dist/esm/sync-store/encode.js.map +1 -0
- package/dist/esm/sync-store/index.js +885 -0
- package/dist/esm/sync-store/index.js.map +1 -0
- package/dist/esm/sync-store/migrations.js +1595 -0
- package/dist/esm/sync-store/migrations.js.map +1 -0
- package/dist/esm/sync-store/schema.js +181 -0
- package/dist/esm/sync-store/schema.js.map +1 -0
- package/dist/esm/types/db.js +2 -0
- package/dist/esm/types/db.js.map +1 -0
- package/dist/esm/types/eth.js +2 -0
- package/dist/esm/types/eth.js.map +1 -0
- package/dist/esm/types/utils.js +2 -0
- package/dist/esm/types/utils.js.map +1 -0
- package/dist/esm/types/virtual.js +2 -0
- package/dist/esm/types/virtual.js.map +1 -0
- package/dist/esm/ui/app.js +157 -0
- package/dist/esm/ui/app.js.map +1 -0
- package/dist/esm/ui/index.js +29 -0
- package/dist/esm/ui/index.js.map +1 -0
- package/dist/esm/ui/patch.js +103 -0
- package/dist/esm/ui/patch.js.map +1 -0
- package/dist/esm/utils/abi.js +55 -0
- package/dist/esm/utils/abi.js.map +1 -0
- package/dist/esm/utils/bigint.js +37 -0
- package/dist/esm/utils/bigint.js.map +1 -0
- package/dist/esm/utils/chains.js +21 -0
- package/dist/esm/utils/chains.js.map +1 -0
- package/dist/esm/utils/checkpoint.js +139 -0
- package/dist/esm/utils/checkpoint.js.map +1 -0
- package/dist/esm/utils/chunk.js +8 -0
- package/dist/esm/utils/chunk.js.map +1 -0
- package/dist/esm/utils/copy.js +129 -0
- package/dist/esm/utils/copy.js.map +1 -0
- package/dist/esm/utils/date.js +27 -0
- package/dist/esm/utils/date.js.map +1 -0
- package/dist/esm/utils/debug.js +2 -0
- package/dist/esm/utils/debug.js.map +1 -0
- package/dist/esm/utils/decodeAbiParameters.js +290 -0
- package/dist/esm/utils/decodeAbiParameters.js.map +1 -0
- package/dist/esm/utils/decodeEventLog.js +75 -0
- package/dist/esm/utils/decodeEventLog.js.map +1 -0
- package/dist/esm/utils/dedupe.js +29 -0
- package/dist/esm/utils/dedupe.js.map +1 -0
- package/dist/esm/utils/duplicates.js +19 -0
- package/dist/esm/utils/duplicates.js.map +1 -0
- package/dist/esm/utils/estimate.js +6 -0
- package/dist/esm/utils/estimate.js.map +1 -0
- package/dist/esm/utils/finality.js +38 -0
- package/dist/esm/utils/finality.js.map +1 -0
- package/dist/esm/utils/format.js +20 -0
- package/dist/esm/utils/format.js.map +1 -0
- package/dist/esm/utils/generators.js +121 -0
- package/dist/esm/utils/generators.js.map +1 -0
- package/dist/esm/utils/hash.js +11 -0
- package/dist/esm/utils/hash.js.map +1 -0
- package/dist/esm/utils/interval.js +171 -0
- package/dist/esm/utils/interval.js.map +1 -0
- package/dist/esm/utils/lowercase.js +7 -0
- package/dist/esm/utils/lowercase.js.map +1 -0
- package/dist/esm/utils/mutex.js +26 -0
- package/dist/esm/utils/mutex.js.map +1 -0
- package/dist/esm/utils/never.js +4 -0
- package/dist/esm/utils/never.js.map +1 -0
- package/dist/esm/utils/offset.js +101 -0
- package/dist/esm/utils/offset.js.map +1 -0
- package/dist/esm/utils/order.js +18 -0
- package/dist/esm/utils/order.js.map +1 -0
- package/dist/esm/utils/partition.js +46 -0
- package/dist/esm/utils/partition.js.map +1 -0
- package/dist/esm/utils/pg.js +149 -0
- package/dist/esm/utils/pg.js.map +1 -0
- package/dist/esm/utils/pglite.js +80 -0
- package/dist/esm/utils/pglite.js.map +1 -0
- package/dist/esm/utils/port.js +30 -0
- package/dist/esm/utils/port.js.map +1 -0
- package/dist/esm/utils/print.js +23 -0
- package/dist/esm/utils/print.js.map +1 -0
- package/dist/esm/utils/promiseAllSettledWithThrow.js +19 -0
- package/dist/esm/utils/promiseAllSettledWithThrow.js.map +1 -0
- package/dist/esm/utils/promiseWithResolvers.js +13 -0
- package/dist/esm/utils/promiseWithResolvers.js.map +1 -0
- package/dist/esm/utils/queue.js +150 -0
- package/dist/esm/utils/queue.js.map +1 -0
- package/dist/esm/utils/range.js +8 -0
- package/dist/esm/utils/range.js.map +1 -0
- package/dist/esm/utils/result.js +10 -0
- package/dist/esm/utils/result.js.map +1 -0
- package/dist/esm/utils/sql-parse.js +1326 -0
- package/dist/esm/utils/sql-parse.js.map +1 -0
- package/dist/esm/utils/timer.js +9 -0
- package/dist/esm/utils/timer.js.map +1 -0
- package/dist/esm/utils/truncate.js +15 -0
- package/dist/esm/utils/truncate.js.map +1 -0
- package/dist/esm/utils/wait.js +10 -0
- package/dist/esm/utils/wait.js.map +1 -0
- package/dist/esm/utils/zipper.js +67 -0
- package/dist/esm/utils/zipper.js.map +1 -0
- package/dist/types/bin/commands/codegen.d.ts +5 -0
- package/dist/types/bin/commands/codegen.d.ts.map +1 -0
- package/dist/types/bin/commands/createViews.d.ts +8 -0
- package/dist/types/bin/commands/createViews.d.ts.map +1 -0
- package/dist/types/bin/commands/dev.d.ts +5 -0
- package/dist/types/bin/commands/dev.d.ts.map +1 -0
- package/dist/types/bin/commands/list.d.ts +5 -0
- package/dist/types/bin/commands/list.d.ts.map +1 -0
- package/dist/types/bin/commands/prune.d.ts +5 -0
- package/dist/types/bin/commands/prune.d.ts.map +1 -0
- package/dist/types/bin/commands/serve.d.ts +5 -0
- package/dist/types/bin/commands/serve.d.ts.map +1 -0
- package/dist/types/bin/commands/start.d.ts +19 -0
- package/dist/types/bin/commands/start.d.ts.map +1 -0
- package/dist/types/bin/isolatedController.d.ts +13 -0
- package/dist/types/bin/isolatedController.d.ts.map +1 -0
- package/dist/types/bin/isolatedWorker.d.ts +9 -0
- package/dist/types/bin/isolatedWorker.d.ts.map +1 -0
- package/dist/types/bin/ponder.d.ts +37 -0
- package/dist/types/bin/ponder.d.ts.map +1 -0
- package/dist/types/bin/utils/codegen.d.ts +6 -0
- package/dist/types/bin/utils/codegen.d.ts.map +1 -0
- package/dist/types/bin/utils/exit.d.ts +10 -0
- package/dist/types/bin/utils/exit.d.ts.map +1 -0
- package/dist/types/build/config.d.ts +97 -0
- package/dist/types/build/config.d.ts.map +1 -0
- package/dist/types/build/factory.d.ts +13 -0
- package/dist/types/build/factory.d.ts.map +1 -0
- package/dist/types/build/index.d.ts +84 -0
- package/dist/types/build/index.d.ts.map +1 -0
- package/dist/types/build/plugin.d.ts +4 -0
- package/dist/types/build/plugin.d.ts.map +1 -0
- package/dist/types/build/pre.d.ts +26 -0
- package/dist/types/build/pre.d.ts.map +1 -0
- package/dist/types/build/schema.d.ts +20 -0
- package/dist/types/build/schema.d.ts.map +1 -0
- package/dist/types/build/stacktrace.d.ts +13 -0
- package/dist/types/build/stacktrace.d.ts.map +1 -0
- package/dist/types/client/index.d.ts +27 -0
- package/dist/types/client/index.d.ts.map +1 -0
- package/dist/types/config/address.d.ts +24 -0
- package/dist/types/config/address.d.ts.map +1 -0
- package/dist/types/config/eventFilter.d.ts +18 -0
- package/dist/types/config/eventFilter.d.ts.map +1 -0
- package/dist/types/config/index.d.ts +149 -0
- package/dist/types/config/index.d.ts.map +1 -0
- package/dist/types/config/utilityTypes.d.ts +43 -0
- package/dist/types/config/utilityTypes.d.ts.map +1 -0
- package/dist/types/database/actions.d.ts +99 -0
- package/dist/types/database/actions.d.ts.map +1 -0
- package/dist/types/database/index.d.ts +481 -0
- package/dist/types/database/index.d.ts.map +1 -0
- package/dist/types/database/queryBuilder.d.ts +65 -0
- package/dist/types/database/queryBuilder.d.ts.map +1 -0
- package/dist/types/drizzle/bigint.d.ts +25 -0
- package/dist/types/drizzle/bigint.d.ts.map +1 -0
- package/dist/types/drizzle/bytes.d.ts +31 -0
- package/dist/types/drizzle/bytes.d.ts.map +1 -0
- package/dist/types/drizzle/hex.d.ts +25 -0
- package/dist/types/drizzle/hex.d.ts.map +1 -0
- package/dist/types/drizzle/index.d.ts +10 -0
- package/dist/types/drizzle/index.d.ts.map +1 -0
- package/dist/types/drizzle/json.d.ts +51 -0
- package/dist/types/drizzle/json.d.ts.map +1 -0
- package/dist/types/drizzle/kit/index.d.ts +189 -0
- package/dist/types/drizzle/kit/index.d.ts.map +1 -0
- package/dist/types/drizzle/onchain.d.ts +287 -0
- package/dist/types/drizzle/onchain.d.ts.map +1 -0
- package/dist/types/drizzle/text.d.ts +29 -0
- package/dist/types/drizzle/text.d.ts.map +1 -0
- package/dist/types/graphql/graphiql.html.d.ts +2 -0
- package/dist/types/graphql/graphiql.html.d.ts.map +1 -0
- package/dist/types/graphql/index.d.ts +12 -0
- package/dist/types/graphql/index.d.ts.map +1 -0
- package/dist/types/graphql/json.d.ts +3 -0
- package/dist/types/graphql/json.d.ts.map +1 -0
- package/dist/types/graphql/middleware.d.ts +29 -0
- package/dist/types/graphql/middleware.d.ts.map +1 -0
- package/dist/types/index.d.ts +23 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/indexing/addStackTrace.d.ts +3 -0
- package/dist/types/indexing/addStackTrace.d.ts.map +1 -0
- package/dist/types/indexing/client.d.ts +154 -0
- package/dist/types/indexing/client.d.ts.map +1 -0
- package/dist/types/indexing/index.d.ts +75 -0
- package/dist/types/indexing/index.d.ts.map +1 -0
- package/dist/types/indexing/profile.d.ts +16 -0
- package/dist/types/indexing/profile.d.ts.map +1 -0
- package/dist/types/indexing-store/cache.d.ts +115 -0
- package/dist/types/indexing-store/cache.d.ts.map +1 -0
- package/dist/types/indexing-store/historical.d.ts +12 -0
- package/dist/types/indexing-store/historical.d.ts.map +1 -0
- package/dist/types/indexing-store/index.d.ts +14 -0
- package/dist/types/indexing-store/index.d.ts.map +1 -0
- package/dist/types/indexing-store/profile.d.ts +7 -0
- package/dist/types/indexing-store/profile.d.ts.map +1 -0
- package/dist/types/indexing-store/realtime.d.ts +10 -0
- package/dist/types/indexing-store/realtime.d.ts.map +1 -0
- package/dist/types/indexing-store/utils.d.ts +19 -0
- package/dist/types/indexing-store/utils.d.ts.map +1 -0
- package/dist/types/internal/common.d.ts +15 -0
- package/dist/types/internal/common.d.ts.map +1 -0
- package/dist/types/internal/errors.d.ts +101 -0
- package/dist/types/internal/errors.d.ts.map +1 -0
- package/dist/types/internal/logger.d.ts +37 -0
- package/dist/types/internal/logger.d.ts.map +1 -0
- package/dist/types/internal/metrics.d.ts +120 -0
- package/dist/types/internal/metrics.d.ts.map +1 -0
- package/dist/types/internal/options.d.ts +62 -0
- package/dist/types/internal/options.d.ts.map +1 -0
- package/dist/types/internal/shutdown.d.ts +8 -0
- package/dist/types/internal/shutdown.d.ts.map +1 -0
- package/dist/types/internal/telemetry.d.ts +43 -0
- package/dist/types/internal/telemetry.d.ts.map +1 -0
- package/dist/types/internal/types.d.ts +435 -0
- package/dist/types/internal/types.d.ts.map +1 -0
- package/dist/types/rpc/actions.d.ts +360 -0
- package/dist/types/rpc/actions.d.ts.map +1 -0
- package/dist/types/rpc/http.d.ts +17 -0
- package/dist/types/rpc/http.d.ts.map +1 -0
- package/dist/types/rpc/index.d.ts +43 -0
- package/dist/types/rpc/index.d.ts.map +1 -0
- package/dist/types/runtime/events.d.ts +40 -0
- package/dist/types/runtime/events.d.ts.map +1 -0
- package/dist/types/runtime/filter.d.ts +87 -0
- package/dist/types/runtime/filter.d.ts.map +1 -0
- package/dist/types/runtime/fragments.d.ts +30 -0
- package/dist/types/runtime/fragments.d.ts.map +1 -0
- package/dist/types/runtime/historical.d.ts +123 -0
- package/dist/types/runtime/historical.d.ts.map +1 -0
- package/dist/types/runtime/index.d.ts +89 -0
- package/dist/types/runtime/index.d.ts.map +1 -0
- package/dist/types/runtime/init.d.ts +28 -0
- package/dist/types/runtime/init.d.ts.map +1 -0
- package/dist/types/runtime/isolated.d.ts +14 -0
- package/dist/types/runtime/isolated.d.ts.map +1 -0
- package/dist/types/runtime/multichain.d.ts +13 -0
- package/dist/types/runtime/multichain.d.ts.map +1 -0
- package/dist/types/runtime/omnichain.d.ts +23 -0
- package/dist/types/runtime/omnichain.d.ts.map +1 -0
- package/dist/types/runtime/realtime.d.ts +93 -0
- package/dist/types/runtime/realtime.d.ts.map +1 -0
- package/dist/types/server/error.d.ts +5 -0
- package/dist/types/server/error.d.ts.map +1 -0
- package/dist/types/server/index.d.ts +13 -0
- package/dist/types/server/index.d.ts.map +1 -0
- package/dist/types/sync-historical/index.d.ts +36 -0
- package/dist/types/sync-historical/index.d.ts.map +1 -0
- package/dist/types/sync-historical/portal-realtime-wire.d.ts +102 -0
- package/dist/types/sync-historical/portal-realtime-wire.d.ts.map +1 -0
- package/dist/types/sync-historical/portal-realtime.d.ts +95 -0
- package/dist/types/sync-historical/portal-realtime.d.ts.map +1 -0
- package/dist/types/sync-historical/portal-transform.d.ts +51 -0
- package/dist/types/sync-historical/portal-transform.d.ts.map +1 -0
- package/dist/types/sync-historical/portal.d.ts +34 -0
- package/dist/types/sync-historical/portal.d.ts.map +1 -0
- package/dist/types/sync-historical/realtime.d.ts +71 -0
- package/dist/types/sync-historical/realtime.d.ts.map +1 -0
- package/dist/types/sync-realtime/bloom.d.ts +18 -0
- package/dist/types/sync-realtime/bloom.d.ts.map +1 -0
- package/dist/types/sync-realtime/index.d.ts +47 -0
- package/dist/types/sync-realtime/index.d.ts.map +1 -0
- package/dist/types/sync-store/encode.d.ts +25 -0
- package/dist/types/sync-store/encode.d.ts.map +1 -0
- package/dist/types/sync-store/index.d.ts +135 -0
- package/dist/types/sync-store/index.d.ts.map +1 -0
- package/dist/types/sync-store/migrations.d.ts +8 -0
- package/dist/types/sync-store/migrations.d.ts.map +1 -0
- package/dist/types/sync-store/schema.d.ts +1828 -0
- package/dist/types/sync-store/schema.d.ts.map +1 -0
- package/dist/types/types/db.d.ts +213 -0
- package/dist/types/types/db.d.ts.map +1 -0
- package/dist/types/types/eth.d.ts +196 -0
- package/dist/types/types/eth.d.ts.map +1 -0
- package/dist/types/types/utils.d.ts +38 -0
- package/dist/types/types/utils.d.ts.map +1 -0
- package/dist/types/types/virtual.d.ts +99 -0
- package/dist/types/types/virtual.d.ts.map +1 -0
- package/dist/types/ui/app.d.ts +22 -0
- package/dist/types/ui/app.d.ts.map +1 -0
- package/dist/types/ui/index.d.ts +5 -0
- package/dist/types/ui/index.d.ts.map +1 -0
- package/dist/types/ui/patch.d.ts +7 -0
- package/dist/types/ui/patch.d.ts.map +1 -0
- package/dist/types/utils/abi.d.ts +23 -0
- package/dist/types/utils/abi.d.ts.map +1 -0
- package/dist/types/utils/bigint.d.ts +15 -0
- package/dist/types/utils/bigint.d.ts.map +1 -0
- package/dist/types/utils/chains.d.ts +42 -0
- package/dist/types/utils/chains.d.ts.map +1 -0
- package/dist/types/utils/checkpoint.d.ts +52 -0
- package/dist/types/utils/checkpoint.d.ts.map +1 -0
- package/dist/types/utils/chunk.d.ts +2 -0
- package/dist/types/utils/chunk.d.ts.map +1 -0
- package/dist/types/utils/copy.d.ts +16 -0
- package/dist/types/utils/copy.d.ts.map +1 -0
- package/dist/types/utils/date.d.ts +7 -0
- package/dist/types/utils/date.d.ts.map +1 -0
- package/dist/types/utils/debug.d.ts +105 -0
- package/dist/types/utils/debug.d.ts.map +1 -0
- package/dist/types/utils/decodeAbiParameters.d.ts +28 -0
- package/dist/types/utils/decodeAbiParameters.d.ts.map +1 -0
- package/dist/types/utils/decodeEventLog.d.ts +12 -0
- package/dist/types/utils/decodeEventLog.d.ts.map +1 -0
- package/dist/types/utils/dedupe.d.ts +20 -0
- package/dist/types/utils/dedupe.d.ts.map +1 -0
- package/dist/types/utils/duplicates.d.ts +7 -0
- package/dist/types/utils/duplicates.d.ts.map +1 -0
- package/dist/types/utils/estimate.d.ts +11 -0
- package/dist/types/utils/estimate.d.ts.map +1 -0
- package/dist/types/utils/finality.d.ts +12 -0
- package/dist/types/utils/finality.d.ts.map +1 -0
- package/dist/types/utils/format.d.ts +3 -0
- package/dist/types/utils/format.d.ts.map +1 -0
- package/dist/types/utils/generators.d.ts +42 -0
- package/dist/types/utils/generators.d.ts.map +1 -0
- package/dist/types/utils/hash.d.ts +11 -0
- package/dist/types/utils/hash.d.ts.map +1 -0
- package/dist/types/utils/interval.d.ts +53 -0
- package/dist/types/utils/interval.d.ts.map +1 -0
- package/dist/types/utils/lowercase.d.ts +5 -0
- package/dist/types/utils/lowercase.d.ts.map +1 -0
- package/dist/types/utils/mutex.d.ts +5 -0
- package/dist/types/utils/mutex.d.ts.map +1 -0
- package/dist/types/utils/never.d.ts +2 -0
- package/dist/types/utils/never.d.ts.map +1 -0
- package/dist/types/utils/offset.d.ts +8 -0
- package/dist/types/utils/offset.d.ts.map +1 -0
- package/dist/types/utils/order.d.ts +2 -0
- package/dist/types/utils/order.d.ts.map +1 -0
- package/dist/types/utils/partition.d.ts +22 -0
- package/dist/types/utils/partition.d.ts.map +1 -0
- package/dist/types/utils/pg.d.ts +8 -0
- package/dist/types/utils/pg.d.ts.map +1 -0
- package/dist/types/utils/pglite.d.ts +25 -0
- package/dist/types/utils/pglite.d.ts.map +1 -0
- package/dist/types/utils/port.d.ts +5 -0
- package/dist/types/utils/port.d.ts.map +1 -0
- package/dist/types/utils/print.d.ts +2 -0
- package/dist/types/utils/print.d.ts.map +1 -0
- package/dist/types/utils/promiseAllSettledWithThrow.d.ts +8 -0
- package/dist/types/utils/promiseAllSettledWithThrow.d.ts.map +1 -0
- package/dist/types/utils/promiseWithResolvers.d.ts +10 -0
- package/dist/types/utils/promiseWithResolvers.d.ts.map +1 -0
- package/dist/types/utils/queue.d.ts +33 -0
- package/dist/types/utils/queue.d.ts.map +1 -0
- package/dist/types/utils/range.d.ts +8 -0
- package/dist/types/utils/range.d.ts.map +1 -0
- package/dist/types/utils/result.d.ts +17 -0
- package/dist/types/utils/result.d.ts.map +1 -0
- package/dist/types/utils/sql-parse.d.ts +21 -0
- package/dist/types/utils/sql-parse.d.ts.map +1 -0
- package/dist/types/utils/timer.d.ts +6 -0
- package/dist/types/utils/timer.d.ts.map +1 -0
- package/dist/types/utils/truncate.d.ts +9 -0
- package/dist/types/utils/truncate.d.ts.map +1 -0
- package/dist/types/utils/wait.d.ts +6 -0
- package/dist/types/utils/wait.d.ts.map +1 -0
- package/dist/types/utils/zipper.d.ts +36 -0
- package/dist/types/utils/zipper.d.ts.map +1 -0
- package/package.json +116 -0
- package/src/bin/commands/codegen.ts +56 -0
- package/src/bin/commands/createViews.ts +311 -0
- package/src/bin/commands/dev.ts +490 -0
- package/src/bin/commands/list.ts +207 -0
- package/src/bin/commands/prune.ts +316 -0
- package/src/bin/commands/serve.ts +236 -0
- package/src/bin/commands/start.ts +319 -0
- package/src/bin/isolatedController.ts +300 -0
- package/src/bin/isolatedWorker.ts +192 -0
- package/src/bin/ponder.ts +200 -0
- package/src/bin/utils/codegen.ts +32 -0
- package/src/bin/utils/exit.ts +112 -0
- package/src/build/config.ts +1136 -0
- package/src/build/factory.ts +122 -0
- package/src/build/index.ts +747 -0
- package/src/build/plugin.ts +58 -0
- package/src/build/pre.ts +100 -0
- package/src/build/schema.ts +291 -0
- package/src/build/stacktrace.ts +137 -0
- package/src/client/index.ts +551 -0
- package/src/config/address.ts +32 -0
- package/src/config/eventFilter.ts +33 -0
- package/src/config/index.ts +245 -0
- package/src/config/utilityTypes.ts +152 -0
- package/src/database/actions.ts +870 -0
- package/src/database/index.ts +1018 -0
- package/src/database/queryBuilder.ts +534 -0
- package/src/drizzle/bigint.ts +57 -0
- package/src/drizzle/bytes.ts +68 -0
- package/src/drizzle/hex.ts +58 -0
- package/src/drizzle/index.ts +58 -0
- package/src/drizzle/json.ts +154 -0
- package/src/drizzle/kit/index.ts +1352 -0
- package/src/drizzle/onchain.ts +447 -0
- package/src/drizzle/text.ts +77 -0
- package/src/graphql/graphiql.html.ts +59 -0
- package/src/graphql/index.ts +1329 -0
- package/src/graphql/json.ts +62 -0
- package/src/graphql/middleware.ts +111 -0
- package/src/index.ts +139 -0
- package/src/indexing/addStackTrace.ts +69 -0
- package/src/indexing/client.ts +1184 -0
- package/src/indexing/index.ts +961 -0
- package/src/indexing/profile.ts +771 -0
- package/src/indexing-store/cache.ts +1056 -0
- package/src/indexing-store/historical.ts +555 -0
- package/src/indexing-store/index.ts +73 -0
- package/src/indexing-store/profile.ts +557 -0
- package/src/indexing-store/realtime.ts +412 -0
- package/src/indexing-store/utils.ts +162 -0
- package/src/internal/common.ts +15 -0
- package/src/internal/errors.ts +228 -0
- package/src/internal/logger.ts +252 -0
- package/src/internal/metrics.ts +1027 -0
- package/src/internal/options.ts +130 -0
- package/src/internal/shutdown.ts +32 -0
- package/src/internal/telemetry.ts +303 -0
- package/src/internal/types.ts +598 -0
- package/src/rpc/actions.ts +1344 -0
- package/src/rpc/http.ts +164 -0
- package/src/rpc/index.ts +959 -0
- package/src/runtime/events.ts +875 -0
- package/src/runtime/filter.ts +664 -0
- package/src/runtime/fragments.ts +674 -0
- package/src/runtime/historical.ts +1556 -0
- package/src/runtime/index.ts +578 -0
- package/src/runtime/init.ts +49 -0
- package/src/runtime/isolated.ts +769 -0
- package/src/runtime/multichain.ts +853 -0
- package/src/runtime/omnichain.ts +913 -0
- package/src/runtime/realtime.ts +1179 -0
- package/src/server/error.ts +68 -0
- package/src/server/index.ts +173 -0
- package/src/sync-historical/index.ts +1062 -0
- package/src/sync-historical/portal-realtime-wire.ts +389 -0
- package/src/sync-historical/portal-realtime.ts +209 -0
- package/src/sync-historical/portal-transform.ts +123 -0
- package/src/sync-historical/portal.ts +811 -0
- package/src/sync-historical/realtime.ts +132 -0
- package/src/sync-realtime/bloom.ts +102 -0
- package/src/sync-realtime/index.ts +1298 -0
- package/src/sync-store/encode.ts +153 -0
- package/src/sync-store/index.ts +1633 -0
- package/src/sync-store/migrations.ts +1801 -0
- package/src/sync-store/schema.ts +248 -0
- package/src/types/db.ts +292 -0
- package/src/types/eth.ts +216 -0
- package/src/types/utils.ts +47 -0
- package/src/types/virtual.ts +244 -0
- package/src/types.d.ts +38 -0
- package/src/ui/app.ts +207 -0
- package/src/ui/index.ts +37 -0
- package/src/ui/patch.ts +145 -0
- package/src/utils/abi.ts +103 -0
- package/src/utils/bigint.ts +41 -0
- package/src/utils/chains.ts +22 -0
- package/src/utils/checkpoint.ts +203 -0
- package/src/utils/chunk.ts +7 -0
- package/src/utils/copy.ts +151 -0
- package/src/utils/date.ts +26 -0
- package/src/utils/debug.ts +110 -0
- package/src/utils/decodeAbiParameters.ts +428 -0
- package/src/utils/decodeEventLog.ts +100 -0
- package/src/utils/dedupe.ts +32 -0
- package/src/utils/duplicates.ts +19 -0
- package/src/utils/estimate.ts +27 -0
- package/src/utils/finality.ts +40 -0
- package/src/utils/format.ts +22 -0
- package/src/utils/generators.ts +157 -0
- package/src/utils/hash.ts +22 -0
- package/src/utils/interval.ts +212 -0
- package/src/utils/lowercase.ts +6 -0
- package/src/utils/mutex.ts +33 -0
- package/src/utils/never.ts +3 -0
- package/src/utils/offset.ts +133 -0
- package/src/utils/order.ts +16 -0
- package/src/utils/partition.ts +53 -0
- package/src/utils/pg.ts +197 -0
- package/src/utils/pglite.ts +97 -0
- package/src/utils/port.ts +34 -0
- package/src/utils/print.ts +31 -0
- package/src/utils/promiseAllSettledWithThrow.ts +27 -0
- package/src/utils/promiseWithResolvers.ts +20 -0
- package/src/utils/queue.ts +258 -0
- package/src/utils/range.ts +8 -0
- package/src/utils/result.ts +26 -0
- package/src/utils/sql-parse.ts +1477 -0
- package/src/utils/timer.ts +8 -0
- package/src/utils/truncate.ts +15 -0
- package/src/utils/wait.ts +8 -0
- 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
|
+
};
|