Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
7bfe63c
feat: define BlockFetcher interface and extract RpcBlockFetcher
wa0x6e Apr 9, 2026
f605ea2
feat: add hypersync_api_token to CheckpointConfig schema
wa0x6e Apr 9, 2026
d032330
refactor: use BlockFetcher abstraction in EvmProvider
wa0x6e Apr 9, 2026
f57d9b3
feat: implement HypersyncBlockFetcher
wa0x6e Apr 9, 2026
c8fce5e
feat: wire fetcher factory with HyperSync support
wa0x6e Apr 9, 2026
2e1e206
test: add block timestamp cache test for EvmProvider
wa0x6e Apr 9, 2026
dce8396
feat: cache full block data from HyperSync to skip getBlock RPC calls
wa0x6e Apr 9, 2026
1262398
refactor: replace @envio-dev/hypersync-client with plain fetch
wa0x6e Apr 9, 2026
576dfa3
refactor: re-export FetchedBlock from evm/types
wa0x6e Apr 9, 2026
30bdfa5
refactor: lazy chainId resolution, revert async init
wa0x6e Apr 9, 2026
487ee57
refactor: separate HyperSync into preloader-only role
wa0x6e Apr 9, 2026
b6bfdc1
refactor: remove BlockFetcher abstraction, restore viem client on pro…
wa0x6e Apr 9, 2026
574d6ff
refactor: extract RpcPreloader, always use preloader plugin
wa0x6e Apr 9, 2026
de7aafe
Revert "refactor: extract RpcPreloader, always use preloader plugin"
wa0x6e Apr 9, 2026
5cbe002
refactor: rename fetchers to preloaders
wa0x6e Apr 9, 2026
345d1be
refactor: simplify hypersync preloader and cache management
wa0x6e Apr 9, 2026
2f74139
refactor: simplify hypersync preloader by inlining helpers
wa0x6e Apr 9, 2026
eb0ffa0
style: apply eslint formatting
wa0x6e Apr 10, 2026
d695ffa
refactor: revert unrelated chunk refactor in provider
wa0x6e Apr 10, 2026
5a56fe9
refactor: extract HyperSync into dedicated HyperSyncEvmProvider
wa0x6e Apr 11, 2026
2e8d349
style: rename hyper-sync files to match camelCase convention
wa0x6e Apr 11, 2026
eaf9fc2
style: group methods by visibility (public, protected, private)
wa0x6e Apr 11, 2026
4b166b7
refactor: use options object for HyperSyncEvmIndexer second argument
wa0x6e Apr 11, 2026
6ca4d86
fix: throw when HyperSync API token is missing
wa0x6e Apr 11, 2026
06971bd
fix: move API token validation to indexer constructor
wa0x6e Apr 11, 2026
9e86c82
style: group methods by visibility in HyperSyncEvmProvider
wa0x6e Apr 11, 2026
039d19e
refactor: remove unnecessary source chunking from HyperSync queries
wa0x6e Apr 11, 2026
b5b96d4
style: use camelCase for HyperSync references
wa0x6e Apr 11, 2026
471799f
refactor: make HyperSync response types non-optional
wa0x6e Apr 11, 2026
32479a1
chore: fix export ordering
wa0x6e Apr 11, 2026
d4a24b6
fix: handle undefined blocks/logs in HyperSync response
wa0x6e Apr 11, 2026
d1d4254
fix: correct HyperSync response parsing and use full preload range
wa0x6e Apr 11, 2026
d56e9dc
style: rename hyper-sync to hypersync in filenames and imports
wa0x6e Apr 13, 2026
3578609
chore: bump version
Sekhmet Apr 13, 2026
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@snapshot-labs/checkpoint",
"version": "0.1.0-beta.68",
"version": "0.1.0-beta.69",
"license": "MIT",
"bin": {
"checkpoint": "dist/src/bin/index.js"
Expand Down
25 changes: 13 additions & 12 deletions src/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,13 +239,12 @@ export class Container implements Instance {
if (this.preloadedBlocks.length > 0)
return this.preloadedBlocks.shift() as number;

const providerRange = this.indexer.getProvider().getPreloadRange();
let currentBlock = blockNum;

while (currentBlock <= this.preloadEndBlock) {
const endBlock = Math.min(
currentBlock + this.preloadStep,
this.preloadEndBlock
);
const step = providerRange ?? this.preloadStep;
const endBlock = Math.min(currentBlock + step, this.preloadEndBlock);
let checkpoints: CheckpointRecord[];
try {
this.log.info(
Expand All @@ -264,14 +263,16 @@ export class Container implements Instance {
continue;
}

const increase =
checkpoints.length > BLOCK_PRELOAD_TARGET
? -BLOCK_PRELOAD_STEP
: +BLOCK_PRELOAD_STEP;
this.preloadStep = Math.max(
BLOCK_RELOAD_MIN_RANGE,
this.preloadStep + increase
);
if (!providerRange) {
const increase =
checkpoints.length > BLOCK_PRELOAD_TARGET
? -BLOCK_PRELOAD_STEP
: +BLOCK_PRELOAD_STEP;
this.preloadStep = Math.max(
BLOCK_RELOAD_MIN_RANGE,
this.preloadStep + increase
);
}

if (checkpoints.length > 0) {
this.preloadedBlocks = [
Expand Down
4 changes: 4 additions & 0 deletions src/providers/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ export class BaseProvider {
);
}

getPreloadRange(): number | null {
return null;
}

async getCheckpointsRange(
fromBlock: number,
toBlock: number
Expand Down
44 changes: 44 additions & 0 deletions src/providers/evm/hypersync-indexer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Logger } from '../../utils/logger';
import { BaseIndexer, Instance } from '../base';
import { HyperSyncEvmProvider } from './hypersync-provider';
import { Writer } from './types';

export class HyperSyncEvmIndexer extends BaseIndexer {
private writers: Record<string, Writer>;
private options: { apiToken: string };

constructor(writers: Record<string, Writer>, options: { apiToken: string }) {
super();

if (!options.apiToken) {
throw new Error('HyperSync API token is required');
}

this.writers = writers;
this.options = options;
}

init({
instance,
log,
abis
}: {
instance: Instance;
log: Logger;
abis?: Record<string, any>;
}) {
log.info('using HyperSync provider');

this.provider = new HyperSyncEvmProvider({
instance,
log,
abis,
writers: this.writers,
apiToken: this.options.apiToken
});
}

public getHandlers(): string[] {
return Object.keys(this.writers);
}
}
220 changes: 220 additions & 0 deletions src/providers/evm/hypersync-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import { Log } from 'viem';
import { EvmProvider } from './provider';
import { Block } from './types';
import { CheckpointRecord } from '../../stores/checkpoints';
import { ContractSourceConfig } from '../../types';

type FetchedBlock = {
number: number;
hash: string;
parentHash: string;
timestamp: number;
};

type HyperSyncBlock = {
number: number;
timestamp: number;
hash: string;
parent_hash: string;
};

type HyperSyncLog = {
block_number: number;
log_index: number;
transaction_index: number;
transaction_hash: string;
block_hash: string;
address: string;
data: string;
topic0: string | null;
topic1: string | null;
topic2: string | null;
topic3: string | null;
removed: boolean;
};

type HyperSyncResponse = {
next_block: number;
data: {
blocks?: HyperSyncBlock[];
logs?: HyperSyncLog[];
}[];
};

const FIELD_SELECTION = {
block: ['number', 'timestamp', 'hash', 'parent_hash'],
log: [
'block_number',
'log_index',
'transaction_index',
'transaction_hash',
'block_hash',
'address',
'data',
'topic0',
'topic1',
'topic2',
'topic3',
'removed'
]
};

export class HyperSyncEvmProvider extends EvmProvider {
private readonly apiToken: string;
private hyperSyncUrl?: string;
private blockCache = new Map<number, FetchedBlock>();

constructor(
params: ConstructorParameters<typeof EvmProvider>[0] & {
apiToken: string;
}
) {
super(params);
this.apiToken = params.apiToken;
}

getPreloadRange(): number {
return Infinity;
}

async getCheckpointsRange(
fromBlock: number,
toBlock: number
): Promise<CheckpointRecord[]> {
const sources = this.instance.getCurrentSources(toBlock);
this.blockCache.clear();

const { logs, blocks } = await this.queryCheckpointsRange(
fromBlock,
toBlock,
sources
);

for (const block of blocks) {
this.blockCache.set(block.number, block);
}

for (const log of logs) {
if (log.blockNumber === null) continue;

if (!this.logsCache.has(log.blockNumber)) {
this.logsCache.set(log.blockNumber, []);
}

this.logsCache.get(log.blockNumber)?.push(log);
}

return logs.map(log => ({
blockNumber: Number(log.blockNumber),
contractAddress: log.address
}));
}

protected async fetchBlock(blockNumber: number): Promise<Block> {
const cached = this.blockCache.get(blockNumber);
if (cached) {
this.blockCache.delete(blockNumber);
return {
number: BigInt(cached.number),
hash: cached.hash,
parentHash: cached.parentHash,
timestamp: BigInt(cached.timestamp)
} as Block;
}

return super.fetchBlock(blockNumber);
}

private async queryCheckpointsRange(
fromBlock: number,
toBlock: number,
sources: ContractSourceConfig[]
): Promise<{ logs: Log[]; blocks: FetchedBlock[] }> {
const allLogs: Log[] = [];
const allBlocks: FetchedBlock[] = [];

const addresses = sources.map(source => source.contract);
const topic0 = sources.flatMap(source =>
source.events.map(event => this.getEventHash(event.name))
);

let currentFrom = fromBlock;
const exclusiveTo = toBlock + 1;

while (currentFrom < exclusiveTo) {
const response = await this.query({
from_block: currentFrom,
to_block: exclusiveTo,
logs: [{ address: addresses, topics: [topic0] }],
field_selection: FIELD_SELECTION
});

for (const chunk of response.data) {
// NOTE: do not replace for/push with spread — spread causes stack overflow on large arrays
for (const block of chunk.blocks ?? []) {
allBlocks.push({
number: block.number,
hash: block.hash,
parentHash: block.parent_hash,
timestamp: block.timestamp
});
}

for (const log of chunk.logs ?? []) {
const topics = [
log.topic0,
log.topic1,
log.topic2,
log.topic3
].filter((t): t is string => !!t) as `0x${string}`[];

allLogs.push({
address: log.address as `0x${string}`,
blockHash: log.block_hash as `0x${string}`,
blockNumber: BigInt(log.block_number),
data: log.data as `0x${string}`,
logIndex: log.log_index,
transactionHash: log.transaction_hash as `0x${string}`,
transactionIndex: log.transaction_index,
removed: log.removed,
topics
} as Log);
}
}

if (response.next_block >= exclusiveTo) break;
currentFrom = response.next_block;
}

return { logs: allLogs, blocks: allBlocks };
}

private async getHyperSyncUrl(): Promise<string> {
if (!this.hyperSyncUrl) {
const chainId = await this.getChainId();
this.hyperSyncUrl = `https://${chainId}.hypersync.xyz`;
}
return this.hyperSyncUrl;
}

private async query(
body: Record<string, unknown>
): Promise<HyperSyncResponse> {
const url = await this.getHyperSyncUrl();

const res = await fetch(`${url}/query`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.apiToken}`
},
body: JSON.stringify(body)
});

if (!res.ok) {
throw new Error(`HyperSync query failed: ${res.statusText}`);
}

return res.json();
}
}
4 changes: 3 additions & 1 deletion src/providers/evm/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export { EvmProvider } from './provider';
export { EvmIndexer } from './indexer';
export { EvmProvider } from './provider';
export { HyperSyncEvmIndexer } from './hypersync-indexer';
export { HyperSyncEvmProvider } from './hypersync-provider';
export * from './types';
Loading
Loading