Skip to content
Merged
1 change: 1 addition & 0 deletions apps/ensindexer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@ensnode/ensnode-sdk": "workspace:*",
"@ensnode/ensrainbow-sdk": "workspace:*",
"@ensnode/ponder-metadata": "workspace:*",
"@ensnode/ponder-sdk": "workspace:*",
"caip": "catalog:",
"date-fns": "catalog:",
"deepmerge-ts": "^7.1.5",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { BlockRef } from "@ensnode/ponder-sdk";

/**
* Backfill scope for a chain.
*
* Defines the range of blocks to be indexed during the backfill phase.
* The backfill phase progresses from `startBlock` toward `endBlock`, and ends
* when the indexed block reaches `endBlock`.
*/
export interface BackfillScope {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe better to implement this as the more generalized concept of a BlockRefRange which would be similar to Blockrange except:

  1. Both fields required.
  2. It's based on BlockRef rather than BlockNumber

Please feel welcome to ignore if you think not sufficiently valuable.

Goal: Make this a more reusable data model in the case we might have a similar goal somewhere else now / later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lightwalker-eth thank you for suggestion. The current specialized vs generalized balance feels right to me. If we need more generic concepts in the future, we can always create them 👍

/**
* The starting block of the backfill range (inclusive).
* This is derived from the chain's indexing configuration.
*/
startBlock: BlockRef;

/**
* The ending block of the backfill range (inclusive).
* This is the "fixed target" that backfill progresses toward.
*
* Guaranteed to be greater than `startBlock`.
*/
endBlock: BlockRef;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { BlockRef } from "@ensnode/ponder-sdk";

export const earliestBlockRef = {
timestamp: 1667260799,
number: 999,
} as const satisfies BlockRef;

export const earlierBlockRef = {
timestamp: 1672531199,
number: 1024,
} as const satisfies BlockRef;

export const laterBlockRef = {
timestamp: 1672531200,
number: 1025,
} as const satisfies BlockRef;

export const latestBlockRef = {
timestamp: 1677104542,
number: 1222,
} as const satisfies BlockRef;
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { type ChainIndexingConfig, ChainIndexingConfigTypeIds } from "@ensnode/ensnode-sdk";
import type {
BlockRef,
ChainIndexingMetrics,
ChainIndexingMetricsCompleted,
ChainIndexingMetricsHistorical,
ChainIndexingMetricsRealtime,
ChainIndexingState,
} from "@ensnode/ponder-sdk";
import { ChainIndexingStates } from "@ensnode/ponder-sdk";

import type { BackfillScope } from "./backfill-scope";
import type { ChainIndexingMetadata } from "./chain-indexing-metadata";

/**
* Build complete indexing metadata for a chain, used for testing the indexing
* status snapshot builders.
*
* @param config - The chain indexing configuration, including backfill scope.
* @param checkpointBlock - The block ref representing the current checkpoint of
* indexing.
* @param state - The current state of indexing (Historical, Realtime, Completed).
* @param latestSyncedBlock - The latest block ref that has been synced (used for
* Realtime state).
* @param backfillEndBlock - The block ref representing the end of the backfill
* scope (used for Historical state).
*
* @returns Complete indexing metadata for the chain.
*/
export function buildChainIndexingMetadataMock({
config,
checkpointBlock,
state,
latestSyncedBlock,
backfillEndBlock,
}: {
config: ChainIndexingConfig;
checkpointBlock: BlockRef;
state: ChainIndexingState;
latestSyncedBlock: BlockRef;
backfillEndBlock: BlockRef;
}): ChainIndexingMetadata {
// assuming historical total blocks as
// `latestBlockRef.number - config.startBlock.number + 1` for the sake of
// this mock, since we don't have real metrics to determine
// the actual end block for historical indexing.
const historicalTotalBlocks = backfillEndBlock.number - config.startBlock.number + 1;

const backfillScope = {
startBlock: config.startBlock,
endBlock:
config.configType === ChainIndexingConfigTypeIds.Definite
? config.endBlock
: backfillEndBlock,
} satisfies BackfillScope;

let indexingMetrics: ChainIndexingMetrics;

switch (state) {
case ChainIndexingStates.Historical:
indexingMetrics = {
state: ChainIndexingStates.Historical,
latestSyncedBlock,
historicalTotalBlocks,
} satisfies ChainIndexingMetricsHistorical;
break;
case ChainIndexingStates.Realtime:
indexingMetrics = {
state: ChainIndexingStates.Realtime,
latestSyncedBlock: latestSyncedBlock,
} satisfies ChainIndexingMetricsRealtime;
break;
case ChainIndexingStates.Completed:
indexingMetrics = {
state: ChainIndexingStates.Completed,
finalIndexedBlock: checkpointBlock,
} satisfies ChainIndexingMetricsCompleted;
break;
}

return {
backfillScope,
indexingConfig: config,
indexingMetrics,
indexingStatus: { checkpointBlock },
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { ChainIndexingConfig } from "@ensnode/ensnode-sdk";
import type { ChainIndexingMetrics, ChainIndexingStatus } from "@ensnode/ponder-sdk";

import type { BackfillScope } from "./backfill-scope";

/**
* Complete indexing metadata for a single chain.
*
* Bundles all data sources describing the chain indexing that are needed to
* build a ChainIndexingStatusSnapshot:
* - The configured backfill scope (what blocks we need to index)
* - Ponder's indexing metrics (current progress/performance)
* - Ponder's indexing status (checkpoint state)
*/
export interface ChainIndexingMetadata {
/**
* Backfill scope for the chain
*
* Defines the range of blocks to be indexed during the backfill phase.
*/
backfillScope: BackfillScope;

/**
* Indexing config for the chain
*
* Defines the range of blocks to be indexed for the chain.
*/
indexingConfig: ChainIndexingConfig;

/**
* Indexing metrics for the chain
*
* References current indexing metrics from Ponder Metrics.
*/
indexingMetrics: ChainIndexingMetrics;

/**
* Indexing status for this chain
*
* References the checkpoint block from Ponder Status.
*/
indexingStatus: ChainIndexingStatus;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { describe, expect, it } from "vitest";

import {
ChainIndexingConfigTypeIds,
ChainIndexingStatusIds,
type ChainIndexingStatusSnapshotBackfill,
type ChainIndexingStatusSnapshotCompleted,
type ChainIndexingStatusSnapshotFollowing,
type ChainIndexingStatusSnapshotQueued,
createIndexingConfig,
} from "@ensnode/ensnode-sdk";
import { ChainIndexingStates } from "@ensnode/ponder-sdk";

import {
earlierBlockRef,
earliestBlockRef,
laterBlockRef,
latestBlockRef,
} from "./block-refs.mock";
import type { ChainIndexingMetadata } from "./chain-indexing-metadata";
import { buildChainIndexingMetadataMock } from "./chain-indexing-metadata.mock";
import { buildChainStatusSnapshots } from "./chain-indexing-status-snapshot";

describe("ChainIndexingStatusSnapshot", () => {
describe("buildChainStatusSnapshots", () => {
it("returns Queued status when checkpointBlock equals startBlock", () => {
// arrange
const metadata = buildChainIndexingMetadataMock({
config: createIndexingConfig(earliestBlockRef, latestBlockRef),
checkpointBlock: earliestBlockRef,
state: ChainIndexingStates.Historical,
latestSyncedBlock: earlierBlockRef,
backfillEndBlock: latestBlockRef,
});

const chainsMetadata = new Map([[1, metadata]]);

// act
const result = buildChainStatusSnapshots(chainsMetadata);

// assert
expect(result.get(1)).toStrictEqual({
chainStatus: ChainIndexingStatusIds.Queued,
config: {
configType: ChainIndexingConfigTypeIds.Definite,
startBlock: earliestBlockRef,
endBlock: latestBlockRef,
},
} satisfies ChainIndexingStatusSnapshotQueued);
});

it("returns Completed status when indexingMetrics.state is Completed", () => {
// arrange
const metadata = buildChainIndexingMetadataMock({
config: createIndexingConfig(earliestBlockRef, latestBlockRef),
checkpointBlock: latestBlockRef,
state: ChainIndexingStates.Completed,
latestSyncedBlock: latestBlockRef,
backfillEndBlock: latestBlockRef,
});

const chainsMetadata = new Map([[1, metadata]]);

// act
const result = buildChainStatusSnapshots(chainsMetadata);

// assert
expect(result.get(1)).toStrictEqual({
chainStatus: ChainIndexingStatusIds.Completed,
config: {
configType: ChainIndexingConfigTypeIds.Definite,
startBlock: earliestBlockRef,
endBlock: latestBlockRef,
},
latestIndexedBlock: latestBlockRef,
} satisfies ChainIndexingStatusSnapshotCompleted);
});

it("returns Following status when indexingMetrics.state is Realtime", () => {
// arrange
const metadata = buildChainIndexingMetadataMock({
config: createIndexingConfig(earliestBlockRef, null),
checkpointBlock: laterBlockRef,
state: ChainIndexingStates.Realtime,
latestSyncedBlock: latestBlockRef,
backfillEndBlock: latestBlockRef,
});

const chainsMetadata = new Map([[1, metadata]]);

// act
const result = buildChainStatusSnapshots(chainsMetadata);

// assert
expect(result.get(1)).toStrictEqual({
chainStatus: ChainIndexingStatusIds.Following,
config: {
configType: ChainIndexingConfigTypeIds.Indefinite,
startBlock: earliestBlockRef,
},
latestIndexedBlock: laterBlockRef,
latestKnownBlock: latestBlockRef,
} satisfies ChainIndexingStatusSnapshotFollowing);
});

it("returns Backfill status for Historical state with definite config", () => {
// arrange
const metadata = buildChainIndexingMetadataMock({
config: createIndexingConfig(earliestBlockRef, latestBlockRef),
checkpointBlock: laterBlockRef,
state: ChainIndexingStates.Historical,
latestSyncedBlock: latestBlockRef,
backfillEndBlock: latestBlockRef,
});

const chainsMetadata = new Map([[1, metadata]]);

// act
const result = buildChainStatusSnapshots(chainsMetadata);

// assert
expect(result.get(1)).toStrictEqual({
chainStatus: ChainIndexingStatusIds.Backfill,
config: {
configType: ChainIndexingConfigTypeIds.Definite,
startBlock: earliestBlockRef,
endBlock: latestBlockRef,
},
latestIndexedBlock: laterBlockRef,
backfillEndBlock: latestBlockRef,
} satisfies ChainIndexingStatusSnapshotBackfill);
});

it("returns Backfill status for Historical state with indefinite config", () => {
// arrange
const metadata = buildChainIndexingMetadataMock({
config: createIndexingConfig(earliestBlockRef, null),
checkpointBlock: earlierBlockRef,
state: ChainIndexingStates.Historical,
latestSyncedBlock: laterBlockRef,
backfillEndBlock: latestBlockRef,
});

const chainsMetadata = new Map([[1, metadata]]);

// act
const result = buildChainStatusSnapshots(chainsMetadata);

// assert
expect(result.get(1)).toStrictEqual({
chainStatus: ChainIndexingStatusIds.Backfill,
config: {
configType: ChainIndexingConfigTypeIds.Indefinite,
startBlock: earliestBlockRef,
},
latestIndexedBlock: earlierBlockRef,
backfillEndBlock: latestBlockRef,
} satisfies ChainIndexingStatusSnapshotBackfill);
});

it("returns empty map when no chains provided", () => {
// arrange
const chainsMetadata = new Map<number, ChainIndexingMetadata>();

// act
const result = buildChainStatusSnapshots(chainsMetadata);

// assert
expect(result.size).toBe(0);
});
});
});
Loading