diff --git a/packages/core/src/build/configAndIndexingFunctions.test.ts b/packages/core/src/build/configAndIndexingFunctions.test.ts index 77cc5c0a9..0b739e9af 100644 --- a/packages/core/src/build/configAndIndexingFunctions.test.ts +++ b/packages/core/src/build/configAndIndexingFunctions.test.ts @@ -42,8 +42,7 @@ test("buildConfigAndIndexingFunctions() builds topics for multiple events", asyn network: { mainnet: {} }, abi: [event0, event1], address: address1, - startBlock: 16370000, - endBlock: 16370020, + blocks: [16370000, 16370020], }, }, }); @@ -72,8 +71,7 @@ test("buildConfigAndIndexingFunctions() handles overloaded event signatures and network: { mainnet: {} }, abi: [event1, event1Overloaded], address: address1, - startBlock: 16370000, - endBlock: 16370020, + blocks: [16370000, 16370020], }, }, }); @@ -102,8 +100,7 @@ test("buildConfigAndIndexingFunctions() handles multiple addresses", async () => network: { mainnet: { address: [address1, address3], - startBlock: 16370000, - endBlock: 16370020, + blocks: [16370000, 16370020], }, }, abi: [event1, event1Overloaded], @@ -163,8 +160,7 @@ test("buildConfigAndIndexingFunctions() builds topics for event filter", async ( }, }, address: address1, - startBlock: 16370000, - endBlock: 16370020, + blocks: [16370000, 16370020], }, }, }); @@ -203,8 +199,7 @@ test("buildConfigAndIndexingFunctions() builds topics for multiple event filters }, ], address: address1, - startBlock: 16370000, - endBlock: 16370020, + blocks: [16370000, 16370020], }, }, }); @@ -237,8 +232,7 @@ test("buildConfigAndIndexingFunctions() overrides default values with network-sp a: { abi: [event0], address: address1, - startBlock: 16370000, - endBlock: 16370020, + blocks: [16370000, 16370020], network: { mainnet: { address: address2, @@ -266,8 +260,7 @@ test("buildConfigAndIndexingFunctions() handles network name shortcut", async () network: "mainnet", abi: [event0], address: address1, - startBlock: 16370000, - endBlock: 16370020, + blocks: [16370000, 16370020], }, }, }); @@ -441,7 +434,7 @@ test("buildConfigAndIndexingFunctions() validates address length", async () => { ); }); -test("buildConfigAndIndexingFunctions() coerces NaN startBlock to undefined", async () => { +test("buildConfigAndIndexingFunctions() coerces NaN startBlock to 0", async () => { const config = createConfig({ networks: { mainnet: { chainId: 1, transport: http("http://127.0.0.1:8545") }, @@ -450,7 +443,7 @@ test("buildConfigAndIndexingFunctions() coerces NaN startBlock to undefined", as a: { network: { mainnet: {} }, abi: [event0, event1], - startBlock: Number.NaN, + blocks: [Number.NaN, 16370020], }, }, }); @@ -460,7 +453,7 @@ test("buildConfigAndIndexingFunctions() coerces NaN startBlock to undefined", as rawIndexingFunctions: [{ name: "a:Event0", fn: () => {} }], }); - expect(sources[0]?.filter.fromBlock).toBe(undefined); + expect(sources[0]?.filter.fromBlock).toBe(0); }); test("buildConfigAndIndexingFunctions() includeTransactionReceipts", async () => { @@ -565,7 +558,7 @@ test("buildConfigAndIndexingFunctions() includeCallTraces with factory", async ( expect(shouldGetTransactionReceipt(sources[0]!.filter)).toBe(false); }); -test("buildConfigAndIndexingFunctions() coerces NaN endBlock to undefined", async () => { +test("buildConfigAndIndexingFunctions() coerces NaN endBlock to Number.MAX_SAFE_INTEGER", async () => { const config = createConfig({ networks: { mainnet: { chainId: 1, transport: http("http://127.0.0.1:8545") }, @@ -574,7 +567,7 @@ test("buildConfigAndIndexingFunctions() coerces NaN endBlock to undefined", asyn a: { network: { mainnet: {} }, abi: [event0, event1], - endBlock: Number.NaN, + blocks: [16370000, Number.NaN], }, }, }); @@ -584,7 +577,58 @@ test("buildConfigAndIndexingFunctions() coerces NaN endBlock to undefined", asyn rawIndexingFunctions: [{ name: "a:Event0", fn: () => {} }], }); - expect(sources[0]!.filter.toBlock).toBe(undefined); + expect(sources[0]!.filter.toBlock).toBe(Number.MAX_SAFE_INTEGER); +}); + +test("buildConfigAndIndexingFunctions() resolves overlapping block ranges", async () => { + const config = createConfig({ + networks: { + mainnet: { chainId: 1, transport: http("http://127.0.0.1:8545") }, + }, + contracts: { + a: { + network: { mainnet: {} }, + abi: [event0, event1], + blocks: [ + [16370000, 16370020], + [16370010, 16370030], + ], + }, + }, + }); + + const { sources } = await buildConfigAndIndexingFunctions({ + config, + rawIndexingFunctions: [{ name: "a:Event0", fn: () => {} }], + }); + + expect(sources[0]!.filter.fromBlock).toBe(16370000); + expect(sources[0]!.filter.toBlock).toBe(16370030); +}); + +test("buildConfigAndIndexingFunctions() multiple block ranges", async () => { + const config = createConfig({ + networks: { + mainnet: { chainId: 1, transport: http("http://127.0.0.1:8545") }, + }, + contracts: { + a: { + network: { mainnet: {} }, + abi: [event0, event1], + blocks: [ + [16370000, 16370020], + [16370040, 16370060], + ], + }, + }, + }); + + const { sources } = await buildConfigAndIndexingFunctions({ + config, + rawIndexingFunctions: [{ name: "a:Event0", fn: () => {} }], + }); + + expect(sources).toHaveLength(2); }); test("buildConfigAndIndexingFunctions() account source", async () => { @@ -596,8 +640,7 @@ test("buildConfigAndIndexingFunctions() account source", async () => { a: { network: { mainnet: {} }, address: address1, - startBlock: 16370000, - endBlock: 16370020, + blocks: [16370000, 16370020], }, }, }); @@ -636,8 +679,7 @@ test("buildConfigAndIndexingFunctions() block source", async () => { blocks: { a: { network: { mainnet: {} }, - startBlock: 16370000, - endBlock: 16370020, + blocks: [16370000, 16370020], }, }, }); diff --git a/packages/core/src/build/configAndIndexingFunctions.ts b/packages/core/src/build/configAndIndexingFunctions.ts index 2e8ebe95c..938a8f1f0 100644 --- a/packages/core/src/build/configAndIndexingFunctions.ts +++ b/packages/core/src/build/configAndIndexingFunctions.ts @@ -24,11 +24,23 @@ import { defaultTransferFilterInclude, } from "@/sync/filter.js"; import { chains } from "@/utils/chains.js"; +import { type Interval, intervalUnion } from "@/utils/interval.js"; import { toLowerCase } from "@/utils/lowercase.js"; +import { _eth_getBlockByNumber } from "@/utils/rpc.js"; import { dedupe } from "@ponder/common"; -import type { Hex, LogTopic } from "viem"; +import { type Hex, type LogTopic, hexToNumber } from "viem"; import { buildLogFactory } from "./factory.js"; +/** + * Block intervals with startBlock (inclusive) and endBlock (inclusive). + * - startBlock: `number | "latest"` + * - endBlock: `number | "latest" | "realtime"` + * - `number`: A specific block number. + * - `"latest"`: The latest block number at the startup of the Ponder instance. + * - `"realtime"`: Indefinite/live indexing. + */ +type BlockRange = [number | "latest", number | "latest" | "realtime"]; + const flattenSources = < T extends Config["contracts"] | Config["accounts"] | Config["blocks"], >( @@ -73,7 +85,59 @@ export async function buildConfigAndIndexingFunctions({ }> { const logs: { level: "warn" | "info" | "debug"; msg: string }[] = []; - const networks: Network[] = await Promise.all( + const latestBlockNumbers = new Map>(); + + const latest = async (network: Network) => { + if (latestBlockNumbers.has(network.name)) { + return hexToNumber(await latestBlockNumbers.get(network.name)!); + } + + const latest: Promise = network.transport.request({ + method: "eth_blockNumber", + }); + + latestBlockNumbers.set(network.name, latest); + + return hexToNumber(await latest); + }; + + const resolveBlockRanges = async ( + blocks: BlockRange[] | BlockRange | undefined, + network: Network, + ) => { + let rawBlockRanges: [number, number | "realtime"][]; + + if (blocks === undefined || blocks.length === 0) { + rawBlockRanges = [[0, "realtime"]]; + } else if (blocks.every((b) => Array.isArray(b))) { + rawBlockRanges = await Promise.all( + blocks.map(async ([fromBlock, toBlock]) => [ + fromBlock === "latest" ? await latest(network) : fromBlock, + toBlock === "latest" ? await latest(network) : toBlock, + ]), + ); + } else { + rawBlockRanges = [ + [ + blocks[0] === "latest" ? await latest(network) : blocks[0], + blocks[1] === "latest" ? await latest(network) : blocks[1], + ], + ]; + } + + const blockRanges: Interval[] = rawBlockRanges.map( + ([rawStartBlock, rawEndBlock]) => [ + Number.isNaN(rawStartBlock) ? 0 : rawStartBlock, + Number.isNaN(rawEndBlock) || rawEndBlock === "realtime" + ? Number.MAX_SAFE_INTEGER + : (rawEndBlock as number), + ], + ); + + return intervalUnion(blockRanges); + }; + + const networks = await Promise.all( Object.entries(config.networks).map(async ([networkName, network]) => { const { chainId, transport } = network; @@ -103,7 +167,7 @@ export async function buildConfigAndIndexingFunctions({ ); } - return { + const resolvedNetwork = { name: networkName, chainId, chain, @@ -113,6 +177,8 @@ export async function buildConfigAndIndexingFunctions({ finalityBlockCount: getFinalityBlockCount({ chainId }), disableCache: network.disableCache ?? false, } satisfies Network; + + return resolvedNetwork; }), ); @@ -217,25 +283,6 @@ export async function buildConfigAndIndexingFunctions({ ); } - const startBlockMaybeNan = source.startBlock; - const startBlock = Number.isNaN(startBlockMaybeNan) - ? undefined - : startBlockMaybeNan; - const endBlockMaybeNan = source.endBlock; - const endBlock = Number.isNaN(endBlockMaybeNan) - ? undefined - : endBlockMaybeNan; - - if ( - startBlock !== undefined && - endBlock !== undefined && - endBlock < startBlock - ) { - throw new Error( - `Validation failed: Start block for '${source.name}' is after end block (${startBlock} > ${endBlock}).`, - ); - } - const network = networks.find((n) => n.name === source.network); if (!network) { throw new Error( @@ -246,10 +293,58 @@ export async function buildConfigAndIndexingFunctions({ .join(", ")}].`, ); } + + const blockRanges: [number, number | "realtime"][] = + source.blocks === undefined + ? [[0, "realtime"]] + : source.blocks.every((b) => Array.isArray(b)) + ? await Promise.all( + source.blocks.map(async ([fromBlock, toBlock]) => [ + fromBlock === "latest" ? await latest(network) : fromBlock, + toBlock === "latest" ? await latest(network) : toBlock, + ]), + ) + : [ + [ + source.blocks[0] === "latest" + ? await latest(network) + : source.blocks[0], + source.blocks[1] === "latest" + ? await latest(network) + : source.blocks[1], + ], + ]; + + for (const [rawStartBlock, rawEndBlock] of blockRanges) { + const startBlock = Number.isNaN(rawStartBlock) ? 0 : rawStartBlock; + const endBlock = Number.isNaN(rawEndBlock) ? "realtime" : rawEndBlock; + if (typeof endBlock !== "string" && endBlock < startBlock) { + throw new Error( + `Validation failed: Start block for '${source.name}' is after end block (${startBlock} > ${endBlock}).`, + ); + } + + if (typeof endBlock === "string" && endBlock !== "realtime") { + throw new Error( + `Validation failed: End block for '${source.name}' is ${endBlock}. Expected number or "realtime"`, + ); + } + } } - const contractSources: ContractSource[] = flattenSources( - config.contracts ?? {}, + const contractSources: ContractSource[] = ( + await Promise.all( + flattenSources(config.contracts ?? {}).map( + async ({ blocks, network, ...rest }) => ({ + blocks: await resolveBlockRanges( + blocks, + networks.find((n) => n.name === network)!, + ), + network, + ...rest, + }), + ), + ) ) .flatMap((source): ContractSource[] => { const network = networks.find((n) => n.name === source.network)!; @@ -377,15 +472,6 @@ export async function buildConfigAndIndexingFunctions({ }); } - const startBlockMaybeNan = source.startBlock; - const fromBlock = Number.isNaN(startBlockMaybeNan) - ? undefined - : startBlockMaybeNan; - const endBlockMaybeNan = source.endBlock; - const toBlock = Number.isNaN(endBlockMaybeNan) - ? undefined - : endBlockMaybeNan; - const contractMetadata = { type: "contract", abi: source.abi, @@ -407,14 +493,92 @@ export async function buildConfigAndIndexingFunctions({ ...resolvedAddress, }); - const logSources = topicsArray.map( - (topics) => + const logSources = topicsArray.flatMap((topics) => + source.blocks.map( + ([fromBlock, toBlock]) => + ({ + ...contractMetadata, + filter: { + type: "log", + chainId: network.chainId, + address: logFactory, + topic0: topics.topic0, + topic1: topics.topic1, + topic2: topics.topic2, + topic3: topics.topic3, + fromBlock, + toBlock, + include: defaultLogFilterInclude.concat( + source.includeTransactionReceipts + ? defaultTransactionReceiptInclude + : [], + ), + }, + }) satisfies ContractSource, + ), + ); + + if (source.includeCallTraces) { + const callTraceSources = source.blocks.map( + ([fromBlock, toBlock]) => + ({ + ...contractMetadata, + filter: { + type: "trace", + chainId: network.chainId, + fromAddress: undefined, + toAddress: logFactory, + callType: "CALL", + functionSelector: registeredFunctionSelectors, + includeReverted: false, + fromBlock, + toBlock, + include: defaultTraceFilterInclude.concat( + source.includeTransactionReceipts + ? defaultTransactionReceiptInclude + : [], + ), + }, + }) satisfies ContractSource, + ); + + return [...logSources, ...callTraceSources]; + } + + return logSources; + } else if (resolvedAddress !== undefined) { + for (const address of Array.isArray(resolvedAddress) + ? resolvedAddress + : [resolvedAddress]) { + if (!address!.startsWith("0x")) + throw new Error( + `Validation failed: Invalid prefix for address '${address}'. Got '${address!.slice( + 0, + 2, + )}', expected '0x'.`, + ); + if (address!.length !== 42) + throw new Error( + `Validation failed: Invalid length for address '${address}'. Got ${address!.length}, expected 42 characters.`, + ); + } + } + + const validatedAddress = Array.isArray(resolvedAddress) + ? dedupe(resolvedAddress).map((r) => toLowerCase(r)) + : resolvedAddress !== undefined + ? toLowerCase(resolvedAddress) + : undefined; + + const logSources = topicsArray.flatMap((topics) => + source.blocks.map( + ([fromBlock, toBlock]) => ({ ...contractMetadata, filter: { type: "log", chainId: network.chainId, - address: logFactory, + address: validatedAddress, topic0: topics.topic0, topic1: topics.topic1, topic2: topics.topic2, @@ -428,18 +592,23 @@ export async function buildConfigAndIndexingFunctions({ ), }, }) satisfies ContractSource, - ); + ), + ); - if (source.includeCallTraces) { - return [ - ...logSources, - { + if (source.includeCallTraces) { + const callTraceSources = source.blocks.map( + ([fromBlock, toBlock]) => + ({ ...contractMetadata, filter: { type: "trace", chainId: network.chainId, fromAddress: undefined, - toAddress: logFactory, + toAddress: Array.isArray(validatedAddress) + ? validatedAddress + : validatedAddress === undefined + ? undefined + : [validatedAddress], callType: "CALL", functionSelector: registeredFunctionSelectors, includeReverted: false, @@ -451,85 +620,10 @@ export async function buildConfigAndIndexingFunctions({ : [], ), }, - } satisfies ContractSource, - ]; - } - - return logSources; - } else if (resolvedAddress !== undefined) { - for (const address of Array.isArray(resolvedAddress) - ? resolvedAddress - : [resolvedAddress]) { - if (!address!.startsWith("0x")) - throw new Error( - `Validation failed: Invalid prefix for address '${address}'. Got '${address!.slice( - 0, - 2, - )}', expected '0x'.`, - ); - if (address!.length !== 42) - throw new Error( - `Validation failed: Invalid length for address '${address}'. Got ${address!.length}, expected 42 characters.`, - ); - } - } - - const validatedAddress = Array.isArray(resolvedAddress) - ? dedupe(resolvedAddress).map((r) => toLowerCase(r)) - : resolvedAddress !== undefined - ? toLowerCase(resolvedAddress) - : undefined; - - const logSources = topicsArray.map( - (topics) => - ({ - ...contractMetadata, - filter: { - type: "log", - chainId: network.chainId, - address: validatedAddress, - topic0: topics.topic0, - topic1: topics.topic1, - topic2: topics.topic2, - topic3: topics.topic3, - fromBlock, - toBlock, - include: defaultLogFilterInclude.concat( - source.includeTransactionReceipts - ? defaultTransactionReceiptInclude - : [], - ), - }, - }) satisfies ContractSource, - ); + }) satisfies ContractSource, + ); - if (source.includeCallTraces) { - return [ - ...logSources, - { - ...contractMetadata, - filter: { - type: "trace", - chainId: network.chainId, - fromAddress: undefined, - toAddress: Array.isArray(validatedAddress) - ? validatedAddress - : validatedAddress === undefined - ? undefined - : [validatedAddress], - callType: "CALL", - functionSelector: registeredFunctionSelectors, - includeReverted: false, - fromBlock, - toBlock, - include: defaultTraceFilterInclude.concat( - source.includeTransactionReceipts - ? defaultTransactionReceiptInclude - : [], - ), - }, - } satisfies ContractSource, - ]; + return [...logSources, ...callTraceSources]; } else return logSources; }) // Remove sources with no registered indexing functions .filter((source) => { @@ -550,19 +644,23 @@ export async function buildConfigAndIndexingFunctions({ return hasNoRegisteredIndexingFunctions === false; }); - const accountSources: AccountSource[] = flattenSources(config.accounts ?? {}) + const accountSources: AccountSource[] = ( + await Promise.all( + flattenSources(config.accounts ?? {}).map( + async ({ blocks, network, ...rest }) => ({ + blocks: await resolveBlockRanges( + blocks, + networks.find((n) => n.name === network)!, + ), + network, + ...rest, + }), + ), + ) + ) .flatMap((source): AccountSource[] => { const network = networks.find((n) => n.name === source.network)!; - const startBlockMaybeNan = source.startBlock; - const fromBlock = Number.isNaN(startBlockMaybeNan) - ? undefined - : startBlockMaybeNan; - const endBlockMaybeNan = source.endBlock; - const toBlock = Number.isNaN(endBlockMaybeNan) - ? undefined - : endBlockMaybeNan; - const resolvedAddress = source?.address; if (resolvedAddress === undefined) { @@ -581,7 +679,7 @@ export async function buildConfigAndIndexingFunctions({ ...resolvedAddress, }); - return [ + const accountSources = source.blocks.flatMap(([fromBlock, toBlock]) => [ { type: "account", name: source.name, @@ -650,7 +748,9 @@ export async function buildConfigAndIndexingFunctions({ ), }, } satisfies AccountSource, - ]; + ]); + + return accountSources; } for (const address of Array.isArray(resolvedAddress) @@ -675,7 +775,7 @@ export async function buildConfigAndIndexingFunctions({ ? toLowerCase(resolvedAddress) : undefined; - return [ + const accountSources = source.blocks.flatMap(([fromBlock, toBlock]) => [ { type: "account", name: source.name, @@ -744,7 +844,9 @@ export async function buildConfigAndIndexingFunctions({ ), }, } satisfies AccountSource, - ]; + ]); + + return accountSources; }) .filter((source) => { const eventName = @@ -767,8 +869,21 @@ export async function buildConfigAndIndexingFunctions({ return hasRegisteredIndexingFunction; }); - const blockSources: BlockSource[] = flattenSources(config.blocks ?? {}) - .map((source) => { + const blockSources: BlockSource[] = ( + await Promise.all( + flattenSources(config.blocks ?? {}).map( + async ({ blocks, network, ...rest }) => ({ + blocks: await resolveBlockRanges( + blocks, + networks.find((n) => n.name === network)!, + ), + network, + ...rest, + }), + ), + ) + ) + .flatMap((source) => { const network = networks.find((n) => n.name === source.network)!; const intervalMaybeNan = source.interval ?? 1; @@ -780,29 +895,23 @@ export async function buildConfigAndIndexingFunctions({ ); } - const startBlockMaybeNan = source.startBlock; - const fromBlock = Number.isNaN(startBlockMaybeNan) - ? undefined - : startBlockMaybeNan; - const endBlockMaybeNan = source.endBlock; - const toBlock = Number.isNaN(endBlockMaybeNan) - ? undefined - : endBlockMaybeNan; - - return { - type: "block", - name: source.name, - network, - filter: { - type: "block", - chainId: network.chainId, - interval: interval, - offset: (fromBlock ?? 0) % interval, - fromBlock, - toBlock, - include: defaultBlockFilterInclude, - }, - } satisfies BlockSource; + return source.blocks.map( + ([fromBlock, toBlock]) => + ({ + type: "block", + name: source.name, + network, + filter: { + type: "block", + chainId: network.chainId, + interval: interval, + offset: fromBlock % interval, + fromBlock, + toBlock, + include: defaultBlockFilterInclude, + }, + }) satisfies BlockSource, + ); }) .filter((blockSource) => { const hasRegisteredIndexingFunction = diff --git a/packages/core/src/config/index.test.ts b/packages/core/src/config/index.test.ts index 05dd94959..56c5373a0 100644 --- a/packages/core/src/config/index.test.ts +++ b/packages/core/src/config/index.test.ts @@ -25,12 +25,12 @@ test("createConfig basic", () => { c1: { abi: [event1], network: "mainnet", - startBlock: 0, + blocks: [0, Number.POSITIVE_INFINITY], }, c2: { abi: [event1], network: "optimism", - startBlock: 0, + blocks: [0, Number.POSITIVE_INFINITY], }, }, }); @@ -185,7 +185,7 @@ test("createConfig network overrides", () => { c1: { abi: [event1], network: "mainnet", - startBlock: 0, + blocks: [0, Number.POSITIVE_INFINITY], }, c2: { abi: [event0, event1], @@ -200,7 +200,7 @@ test("createConfig network overrides", () => { }, }, }, - startBlock: 0, + blocks: [0, Number.POSITIVE_INFINITY], }, }, }); diff --git a/packages/core/src/config/index.ts b/packages/core/src/config/index.ts index 3dfd31b10..5d849bdc3 100644 --- a/packages/core/src/config/index.ts +++ b/packages/core/src/config/index.ts @@ -61,10 +61,17 @@ type DatabaseConfig = // base type BlockConfig = { - /** Block number at which to start indexing events (inclusive). If `undefined`, events will be processed from block 0. Default: `undefined`. */ - startBlock?: number; - /** Block number at which to stop indexing events (inclusive). If `undefined`, events will be processed in real-time. Default: `undefined`. */ - endBlock?: number; + /** + * Block intervals with startBlock (inclusive) and endBlock (inclusive). + * - startBlock: `number | "latest"` + * - endBlock: `number | "latest" | "realtime"` + * - `number`: A specific block number. + * - `"latest"`: The latest block number at the startup of the Ponder instance. + * - `"realtime"`: Indefinite/live indexing. + */ + blocks?: + | [number | "latest", number | "realtime" | "latest"][] + | [number | "latest", number | "realtime" | "latest"]; }; type TransactionReceiptConfig = { @@ -210,11 +217,7 @@ type AccountsConfig = {} extends accounts // blocks -type BlockFilterConfig = { - /** Block number at which to start indexing events (inclusive). If `undefined`, events will be processed from block 0. Default: `undefined`. */ - startBlock?: number; - /** Block number at which to stop indexing events (inclusive). If `undefined`, events will be processed in real-time. Default: `undefined`. */ - endBlock?: number; +type BlockFilterConfig = BlockConfig & { interval?: number; }; diff --git a/packages/core/src/internal/types.ts b/packages/core/src/internal/types.ts index 634ee7f4a..1bdfaed98 100644 --- a/packages/core/src/internal/types.ts +++ b/packages/core/src/internal/types.ts @@ -69,8 +69,8 @@ export type LogFilter< topic1: LogTopic; topic2: LogTopic; topic3: LogTopic; - fromBlock: number | undefined; - toBlock: number | undefined; + fromBlock: number; + toBlock: number; include: | ( | `block.${keyof Block}` @@ -86,8 +86,8 @@ export type BlockFilter = { chainId: number; interval: number; offset: number; - fromBlock: number | undefined; - toBlock: number | undefined; + fromBlock: number; + toBlock: number; include: `block.${keyof Block}`[] | undefined; }; @@ -100,8 +100,8 @@ export type TransferFilter< fromAddress: FilterAddress; toAddress: FilterAddress; includeReverted: boolean; - fromBlock: number | undefined; - toBlock: number | undefined; + fromBlock: number; + toBlock: number; include: | ( | `block.${keyof Block}` @@ -121,8 +121,8 @@ export type TransactionFilter< fromAddress: FilterAddress; toAddress: FilterAddress; includeReverted: boolean; - fromBlock: number | undefined; - toBlock: number | undefined; + fromBlock: number; + toBlock: number; include: | ( | `block.${keyof Block}` @@ -143,8 +143,8 @@ export type TraceFilter< functionSelector: Hex | Hex[] | undefined; callType: Trace["type"] | undefined; includeReverted: boolean; - fromBlock: number | undefined; - toBlock: number | undefined; + fromBlock: number; + toBlock: number; include: | ( | `block.${keyof Block}` diff --git a/packages/core/src/sync-historical/index.ts b/packages/core/src/sync-historical/index.ts index 5c0e79dca..b5c98fdcc 100644 --- a/packages/core/src/sync-historical/index.ts +++ b/packages/core/src/sync-historical/index.ts @@ -758,16 +758,13 @@ export const createHistoricalSync = async ( // is only partially synced. for (const { filter } of args.sources) { - if ( - (filter.fromBlock !== undefined && filter.fromBlock > _interval[1]) || - (filter.toBlock !== undefined && filter.toBlock < _interval[0]) - ) { + if (filter.fromBlock > _interval[1] || filter.toBlock < _interval[0]) { continue; } const interval: Interval = [ - Math.max(filter.fromBlock ?? 0, _interval[0]), - Math.min(filter.toBlock ?? Number.POSITIVE_INFINITY, _interval[1]), + Math.max(filter.fromBlock, _interval[0]), + Math.min(filter.toBlock, _interval[1]), ]; const completedIntervals = intervalsCache.get(filter)!; diff --git a/packages/core/src/sync-realtime/bloom.ts b/packages/core/src/sync-realtime/bloom.ts index 2d636872e..3c1a5178f 100644 --- a/packages/core/src/sync-realtime/bloom.ts +++ b/packages/core/src/sync-realtime/bloom.ts @@ -43,8 +43,8 @@ export function isFilterInBloom({ }): boolean { // Return `false` for out of range blocks if ( - hexToNumber(block.number) < (filter.fromBlock ?? 0) || - hexToNumber(block.number) > (filter.toBlock ?? Number.POSITIVE_INFINITY) + hexToNumber(block.number) < filter.fromBlock || + hexToNumber(block.number) > filter.toBlock ) { return false; } diff --git a/packages/core/src/sync-store/index.test.ts b/packages/core/src/sync-store/index.test.ts index 2d78c753d..418c16052 100644 --- a/packages/core/src/sync-store/index.test.ts +++ b/packages/core/src/sync-store/index.test.ts @@ -80,8 +80,8 @@ test("getIntervals() empty", async (context) => { chainId: 1, interval: 1, offset: 0, - fromBlock: undefined, - toBlock: undefined, + fromBlock: 0, + toBlock: Number.POSITIVE_INFINITY, include: [], } satisfies BlockFilter; @@ -93,11 +93,11 @@ test("getIntervals() empty", async (context) => { Map { { "chainId": 1, - "fromBlock": undefined, + "fromBlock": 0, "include": [], "interval": 1, "offset": 0, - "toBlock": undefined, + "toBlock": Infinity, "type": "block", } => [ { @@ -124,8 +124,8 @@ test("getIntervals() returns intervals", async (context) => { chainId: 1, interval: 1, offset: 0, - fromBlock: undefined, - toBlock: undefined, + fromBlock: 0, + toBlock: Number.POSITIVE_INFINITY, include: [], } satisfies BlockFilter; @@ -147,11 +147,11 @@ test("getIntervals() returns intervals", async (context) => { Map { { "chainId": 1, - "fromBlock": undefined, + "fromBlock": 0, "include": [], "interval": 1, "offset": 0, - "toBlock": undefined, + "toBlock": Infinity, "type": "block", } => [ { @@ -183,8 +183,8 @@ test("getIntervals() merges intervals", async (context) => { chainId: 1, interval: 1, offset: 0, - fromBlock: undefined, - toBlock: undefined, + fromBlock: 0, + toBlock: Number.POSITIVE_INFINITY, include: [], } satisfies BlockFilter; @@ -215,11 +215,11 @@ test("getIntervals() merges intervals", async (context) => { Map { { "chainId": 1, - "fromBlock": undefined, + "fromBlock": 0, "include": [], "interval": 1, "offset": 0, - "toBlock": undefined, + "toBlock": Infinity, "type": "block", } => [ { @@ -254,8 +254,8 @@ test("getIntervals() adjacent intervals", async (context) => { topic2: null, topic3: null, address: [zeroAddress], - fromBlock: undefined, - toBlock: undefined, + fromBlock: 0, + toBlock: Number.POSITIVE_INFINITY, include: [], } satisfies LogFilter; @@ -290,9 +290,9 @@ test("getIntervals() adjacent intervals", async (context) => { "0x0000000000000000000000000000000000000000", ], "chainId": 1, - "fromBlock": undefined, + "fromBlock": 0, "include": [], - "toBlock": undefined, + "toBlock": Infinity, "topic0": null, "topic1": null, "topic2": null, @@ -332,8 +332,8 @@ test("insertIntervals() merges duplicates", async (context) => { chainId: 1, interval: 1, offset: 0, - fromBlock: undefined, - toBlock: undefined, + fromBlock: 0, + toBlock: Number.POSITIVE_INFINITY, include: [], } satisfies BlockFilter; @@ -369,11 +369,11 @@ test("insertIntervals() merges duplicates", async (context) => { Map { { "chainId": 1, - "fromBlock": undefined, + "fromBlock": 0, "include": [], "interval": 1, "offset": 0, - "toBlock": undefined, + "toBlock": Infinity, "type": "block", } => [ { @@ -408,8 +408,8 @@ test("insertIntervals() preserves fragments", async (context) => { topic2: null, topic3: null, address: [zeroAddress, ALICE], - fromBlock: undefined, - toBlock: undefined, + fromBlock: 0, + toBlock: Number.POSITIVE_INFINITY, include: [], } satisfies LogFilter; @@ -435,9 +435,9 @@ test("insertIntervals() preserves fragments", async (context) => { "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", ], "chainId": 1, - "fromBlock": undefined, + "fromBlock": 0, "include": [], - "toBlock": undefined, + "toBlock": Infinity, "topic0": null, "topic1": null, "topic2": null, @@ -1404,8 +1404,8 @@ test("getEvents() returns events", async (context) => { topic1: null, topic2: null, topic3: null, - fromBlock: undefined, - toBlock: undefined, + fromBlock: 0, + toBlock: Number.POSITIVE_INFINITY, include: [], } satisfies LogFilter; diff --git a/packages/core/src/sync/filter.ts b/packages/core/src/sync/filter.ts index dc40f84ab..9777850a8 100644 --- a/packages/core/src/sync/filter.ts +++ b/packages/core/src/sync/filter.ts @@ -124,8 +124,8 @@ export const isLogFilterMatched = ({ }): boolean => { // Return `false` for out of range blocks if ( - hexToNumber(block.number) < (filter.fromBlock ?? 0) || - hexToNumber(block.number) > (filter.toBlock ?? Number.POSITIVE_INFINITY) + hexToNumber(block.number) < filter.fromBlock || + hexToNumber(block.number) > filter.toBlock ) { return false; } @@ -176,8 +176,8 @@ export const isTransactionFilterMatched = ({ }): boolean => { // Return `false` for out of range blocks if ( - hexToNumber(block.number) < (filter.fromBlock ?? 0) || - hexToNumber(block.number) > (filter.toBlock ?? Number.POSITIVE_INFINITY) + hexToNumber(block.number) < filter.fromBlock || + hexToNumber(block.number) > filter.toBlock ) { return false; } @@ -260,8 +260,8 @@ export const isTraceFilterMatched = ({ }): boolean => { // Return `false` for out of range blocks if ( - hexToNumber(block.number) < (filter.fromBlock ?? 0) || - hexToNumber(block.number) > (filter.toBlock ?? Number.POSITIVE_INFINITY) + hexToNumber(block.number) < filter.fromBlock || + hexToNumber(block.number) > filter.toBlock ) { return false; } @@ -345,8 +345,8 @@ export const isTransferFilterMatched = ({ }): boolean => { // Return `false` for out of range blocks if ( - hexToNumber(block.number) < (filter.fromBlock ?? 0) || - hexToNumber(block.number) > (filter.toBlock ?? Number.POSITIVE_INFINITY) + hexToNumber(block.number) < filter.fromBlock || + hexToNumber(block.number) > filter.toBlock ) { return false; } @@ -422,8 +422,8 @@ export const isBlockFilterMatched = ({ }): boolean => { // Return `false` for out of range blocks if ( - hexToNumber(block.number) < (filter.fromBlock ?? 0) || - hexToNumber(block.number) > (filter.toBlock ?? Number.POSITIVE_INFINITY) + hexToNumber(block.number) < filter.fromBlock || + hexToNumber(block.number) > filter.toBlock ) { return false; } diff --git a/packages/core/src/sync/index.ts b/packages/core/src/sync/index.ts index 5f727e43e..4ada0e0cd 100644 --- a/packages/core/src/sync/index.ts +++ b/packages/core/src/sync/index.ts @@ -82,7 +82,7 @@ export type RealtimeEvent = export type SyncProgress = { start: SyncBlock | LightBlock; - end: SyncBlock | LightBlock | undefined; + end: SyncBlock | LightBlock; cached: SyncBlock | LightBlock | undefined; current: SyncBlock | LightBlock | undefined; finalized: SyncBlock | LightBlock; @@ -119,7 +119,7 @@ export const blockToCheckpoint = ( * sync progress has reached the final end block. */ const isSyncEnd = (syncProgress: SyncProgress) => { - if (syncProgress.end === undefined || syncProgress.current === undefined) { + if (syncProgress.current === undefined) { return false; } @@ -145,12 +145,10 @@ const isSyncFinalized = (syncProgress: SyncProgress) => { const getHistoricalLast = ( syncProgress: Pick, ) => { - return syncProgress.end === undefined + return hexToNumber(syncProgress.end.number) > + hexToNumber(syncProgress.finalized.number) ? syncProgress.finalized - : hexToNumber(syncProgress.end.number) > - hexToNumber(syncProgress.finalized.number) - ? syncProgress.finalized - : syncProgress.end; + : syncProgress.end; }; /** Compute the minimum checkpoint, filtering out undefined */ @@ -199,10 +197,6 @@ export const getChainCheckpoint = ({ network: Network; tag: "start" | "current" | "finalized" | "end"; }): string | undefined => { - if (tag === "end" && syncProgress.end === undefined) { - return undefined; - } - if (tag === "current" && isSyncEnd(syncProgress)) { return undefined; } @@ -371,10 +365,6 @@ export const createSync = async (args: CreateSyncParameters): Promise => { getChainCheckpoint({ syncProgress, network, tag }), ); - if (tag === "end" && checkpoints.some((c) => c === undefined)) { - return undefined; - } - if (tag === "current" && checkpoints.every((c) => c === undefined)) { return undefined; } @@ -1010,14 +1000,9 @@ export const syncDiagnostic = async ({ requestQueue: RequestQueue; }) => { /** Earliest `startBlock` among all `filters` */ - const start = Math.min(...sources.map(({ filter }) => filter.fromBlock ?? 0)); - /** - * Latest `endBlock` among all filters. `undefined` if at least one - * of the filters doesn't have an `endBlock`. - */ - const end = sources.some(({ filter }) => filter.toBlock === undefined) - ? undefined - : Math.max(...sources.map(({ filter }) => filter.toBlock!)); + const start = Math.min(...sources.map(({ filter }) => filter.fromBlock)); + /** Latest `endBlock` among all `filters`. */ + const end = Math.max(...sources.map(({ filter }) => filter.toBlock)); const [remoteChainId, startBlock, latestBlock] = await Promise.all([ requestQueue.request({ method: "eth_chainId" }), @@ -1026,16 +1011,14 @@ export const syncDiagnostic = async ({ ]); const endBlock = - end === undefined - ? undefined - : end > hexToBigInt(latestBlock.number) - ? ({ - number: toHex(end), - hash: "0x", - parentHash: "0x", - timestamp: toHex(maxCheckpoint.blockTimestamp), - } as LightBlock) - : await _eth_getBlockByNumber(requestQueue, { blockNumber: end }); + end > hexToBigInt(latestBlock.number) + ? ({ + number: toHex(end), + hash: "0x", + parentHash: "0x", + timestamp: toHex(maxCheckpoint.blockTimestamp), + } as LightBlock) + : await _eth_getBlockByNumber(requestQueue, { blockNumber: end }); // Warn if the config has a different chainId than the remote. if (hexToNumber(remoteChainId) !== network.chainId) { @@ -1073,8 +1056,8 @@ export const getCachedBlock = ({ }): Promise | undefined => { const latestCompletedBlocks = sources.map(({ filter }) => { const requiredInterval = [ - filter.fromBlock ?? 0, - filter.toBlock ?? Number.POSITIVE_INFINITY, + filter.fromBlock, + filter.toBlock, ] satisfies Interval; const fragmentIntervals = historicalSync.intervalsCache.get(filter)!; @@ -1090,7 +1073,7 @@ export const getCachedBlock = ({ if (completedIntervals.length === 0) return undefined; const earliestCompletedInterval = completedIntervals[0]!; - if (earliestCompletedInterval[0] !== (filter.fromBlock ?? 0)) { + if (earliestCompletedInterval[0] !== filter.fromBlock) { return undefined; } return earliestCompletedInterval[1]; @@ -1109,8 +1092,7 @@ export const getCachedBlock = ({ if ( latestCompletedBlocks.every( (block, i) => - block !== undefined || - (sources[i]!.filter.fromBlock ?? 0) > minCompletedBlock, + block !== undefined || sources[i]!.filter.fromBlock > minCompletedBlock, ) ) { return _eth_getBlockByNumber(requestQueue, { @@ -1177,15 +1159,7 @@ export async function* localHistoricalSyncGenerator({ historicalSync.intervalsCache.entries(), ).flatMap(([filter, fragmentIntervals]) => intervalDifference( - [ - [ - filter.fromBlock ?? 0, - Math.min( - filter.toBlock ?? Number.POSITIVE_INFINITY, - totalInterval[1], - ), - ], - ], + [[filter.fromBlock, Math.min(filter.toBlock, totalInterval[1])]], intervalIntersectionMany( fragmentIntervals.map(({ intervals }) => intervals), ), diff --git a/packages/core/src/types/virtual.test-d.ts b/packages/core/src/types/virtual.test-d.ts index bfa28130a..00575b583 100644 --- a/packages/core/src/types/virtual.test-d.ts +++ b/packages/core/src/types/virtual.test-d.ts @@ -55,7 +55,7 @@ const config = createConfig({ abi: [event0, func0], network: "mainnet", address: "0x", - startBlock: 0, + blocks: [0, Number.POSITIVE_INFINITY], includeTransactionReceipts: false, includeCallTraces: true, }, @@ -64,7 +64,7 @@ const config = createConfig({ address: "0x69", network: { mainnet: { - startBlock: 1, + blocks: [1, Number.POSITIVE_INFINITY], includeTransactionReceipts: true, includeCallTraces: true, }, @@ -81,7 +81,7 @@ const config = createConfig({ blocks: { b1: { interval: 2, - startBlock: 1, + blocks: [1, Number.POSITIVE_INFINITY], network: "mainnet", }, }, @@ -283,18 +283,14 @@ test("Context contracts", () => { // ^? type expectedAbi = [Event1, Event1Overloaded, Func1, Func1Overloaded]; - type expectedStartBlock = 1 | undefined; - type expectedEndBlock = undefined; + type expectedBlocks = [1, number] | undefined; type expectedAddress = "0x69"; assertType({} as any as expectedAbi); assertType({} as any as a["abi"]); - assertType({} as any as expectedStartBlock); - assertType({} as any as a["startBlock"]); - - assertType({} as any as expectedEndBlock); - assertType({} as any as a["endBlock"]); + assertType({} as any as expectedBlocks); + assertType({} as any as a["blocks"]); assertType({} as any as expectedAddress); assertType({} as any as a["address"]); diff --git a/packages/core/src/types/virtual.ts b/packages/core/src/types/virtual.ts index b2309428d..436c135f7 100644 --- a/packages/core/src/types/virtual.ts +++ b/packages/core/src/types/virtual.ts @@ -189,13 +189,9 @@ export namespace Virtual { config["contracts"][_contractName], "address" >; - startBlock: ExtractOverridenProperty< + blocks: ExtractOverridenProperty< config["contracts"][_contractName], - "startBlock" - >; - endBlock: ExtractOverridenProperty< - config["contracts"][_contractName], - "endBlock" + "blocks" >; }; };