Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion apps/s03-indexer/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ const sorobanEndpoint =
contractConfig?.network?.sorobanRpcEndpoint ??
"https://soroban-testnet.stellar.org";
const indexedContractIds = getIndexedContractIds(contractConfig);
const startBlock = resolveIndexerStartLedger({
configuredGenesis: process.env.INDEXER_START_LEDGER,
checkpointPath: process.env.INDEXER_CHECKPOINT_PATH,
});

/* This is your project configuration */
const project: StellarProject = {
Expand Down Expand Up @@ -93,7 +97,7 @@ const project: StellarProject = {
/* Set this as a logical start block, it might be block 1 (genesis) or when your
contract was deployed. Override with INDEXER_START_LEDGER for local runs, where
a fresh standalone network must backfill from an early ledger. */
startBlock: Number(process.env.INDEXER_START_LEDGER ?? 228206),
startBlock,
mapping: {
file: "./dist/index.js",
handlers: indexedContractIds.map((contractId) => ({
Expand All @@ -111,6 +115,29 @@ const project: StellarProject = {
// Must set default to the project instance
export default project;

export type IndexerStartLedgerOptions = {
configuredGenesis?: string | number;
checkpointPath?: string;
exists?: (path: string) => boolean;
readFile?: (path: string) => string;
};

export function resolveIndexerStartLedger({
configuredGenesis,
checkpointPath,
exists = existsSync,
readFile = (filePath) => readFileSync(filePath, "utf8"),
}: IndexerStartLedgerOptions = {}): number {
const genesisLedger = parseLedger(configuredGenesis ?? 228206, "configured genesis ledger");

if (!checkpointPath || !exists(checkpointPath)) {
return genesisLedger;
}

const checkpointLedger = readCheckpointLedger(readFile(checkpointPath));
return parseLedger(checkpointLedger, "stored checkpoint ledger");
}

function loadContractConfig(): IndexerContractsConfig | undefined {
const network = process.env.INDEXER_NETWORK ?? "testnet";
const configPath = process.env.INDEXER_CONTRACTS_CONFIG
Expand Down Expand Up @@ -146,3 +173,30 @@ function getIndexedContractIds(config: IndexerContractsConfig | undefined): stri

return [...ids].sort();
}

function readCheckpointLedger(contents: string): unknown {
const trimmed = contents.trim();
if (!trimmed) {
return undefined;
}

try {
const checkpoint = JSON.parse(trimmed) as Record<string, unknown>;
return (
checkpoint.lastProcessedLedger ??
checkpoint.lastProcessedHeight ??
checkpoint.ledger ??
checkpoint.height
);
} catch {
return trimmed;
}
}

function parseLedger(value: unknown, label: string): number {
const ledger = Number(value);
if (!Number.isInteger(ledger) || ledger < 1) {
throw new Error(`Invalid ${label}: ${value}`);
}
return ledger;
}
39 changes: 39 additions & 0 deletions apps/s03-indexer/tests/start-ledger.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, expect, test } from "bun:test";
import { resolveIndexerStartLedger } from "../project";

describe("indexer start ledger resolution", () => {
test("resumes from a stored checkpoint ledger", () => {
const startLedger = resolveIndexerStartLedger({
configuredGenesis: 228206,
checkpointPath: "/tmp/indexer-checkpoint.json",
exists: () => true,
readFile: () => JSON.stringify({ lastProcessedLedger: 314159 }),
});

expect(startLedger).toBe(314159);
});

test("falls back to the configured genesis ledger when checkpoint is missing", () => {
const startLedger = resolveIndexerStartLedger({
configuredGenesis: 228206,
checkpointPath: "/tmp/missing-checkpoint.json",
exists: () => false,
readFile: () => {
throw new Error("checkpoint should not be read");
},
});

expect(startLedger).toBe(228206);
});

test("accepts SubQuery metadata checkpoint height", () => {
const startLedger = resolveIndexerStartLedger({
configuredGenesis: 1,
checkpointPath: "/tmp/subquery-metadata.json",
exists: () => true,
readFile: () => JSON.stringify({ lastProcessedHeight: 987654 }),
});

expect(startLedger).toBe(987654);
});
});