Skip to content

Commit 8904d36

Browse files
authored
refactor(evm): port EVM provider to viem (#377)
* refactor: port ethers utils to viem * refactor: replace JsonRpcProvider with viem PublicClient * refactor: use viem formatter in _getLogs * refactor: move log parsing to viem * feat: add timeout for getLogs for blockHash requests * chore: bump version to 0.1.0-beta.66
1 parent b66eeb0 commit 8904d36

File tree

4 files changed

+199
-365
lines changed

4 files changed

+199
-365
lines changed

package.json

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@snapshot-labs/checkpoint",
3-
"version": "0.1.0-beta.65",
3+
"version": "0.1.0-beta.66",
44
"license": "MIT",
55
"bin": {
66
"checkpoint": "dist/src/bin/index.js"
@@ -19,11 +19,6 @@
1919
},
2020
"prettier": "@snapshot-labs/prettier-config",
2121
"dependencies": {
22-
"@ethersproject/abi": "^5.7.0",
23-
"@ethersproject/address": "^5.7.0",
24-
"@ethersproject/keccak256": "^5.7.0",
25-
"@ethersproject/providers": "^5.7.2",
26-
"@ethersproject/strings": "^5.7.0",
2722
"@graphql-tools/schema": "^8.5.1",
2823
"@starknet-io/types-js": "^0.7.10",
2924
"connection-string": "^4.3.5",
@@ -40,6 +35,7 @@
4035
"pino-pretty": "^13.1.1",
4136
"pluralize": "^8.0.0",
4237
"starknet": "~5.19.3",
38+
"viem": "^2.36.0",
4339
"yargs": "^17.7.2",
4440
"zod": "^3.21.4"
4541
},

src/providers/evm/provider.ts

Lines changed: 80 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
import { Interface, LogDescription } from '@ethersproject/abi';
2-
import { getAddress } from '@ethersproject/address';
3-
import { keccak256 } from '@ethersproject/keccak256';
41
import {
5-
Formatter,
2+
createPublicClient,
3+
formatLog,
4+
getAddress,
5+
http,
6+
keccak256,
67
Log,
7-
Provider,
8-
StaticJsonRpcProvider
9-
} from '@ethersproject/providers';
10-
import { toUtf8Bytes } from '@ethersproject/strings';
8+
parseEventLogs,
9+
ParseEventLogsReturnType,
10+
PublicClient,
11+
RpcLog,
12+
stringToBytes
13+
} from 'viem';
1114
import { getRangeHint } from './helpers';
1215
import { Block, CustomJsonRpcError, EventsData, Writer } from './types';
1316
import { CheckpointRecord } from '../../stores/checkpoints';
@@ -24,17 +27,20 @@ type GetLogsBlockRangeFilter = {
2427
toBlock: number;
2528
};
2629

30+
/**
31+
* Timeout for client requests in milliseconds.
32+
* This timeout is also used when fetching latest blocks in getLogs.
33+
*/
34+
const CLIENT_TIMEOUT = 5 * 1000;
35+
2736
const MAX_BLOCKS_PER_REQUEST = 10000;
2837

2938
export class EvmProvider extends BaseProvider {
30-
private readonly provider: Provider;
31-
/**
32-
* Formatter instance from ethers.js used to format raw responses.
33-
*/
34-
private readonly formatter = new Formatter();
39+
private readonly client: PublicClient;
40+
3541
private readonly writers: Record<string, Writer>;
3642
private sourceHashes = new Map<string, string>();
37-
private logsCache = new Map<number, Log[]>();
43+
private logsCache = new Map<bigint, Log[]>();
3844

3945
constructor({
4046
instance,
@@ -46,9 +52,12 @@ export class EvmProvider extends BaseProvider {
4652
}) {
4753
super({ instance, log, abis });
4854

49-
this.provider = new StaticJsonRpcProvider(
50-
this.instance.config.network_node_url
51-
);
55+
this.client = createPublicClient({
56+
transport: http(instance.config.network_node_url, {
57+
timeout: CLIENT_TIMEOUT
58+
})
59+
});
60+
5261
this.writers = writers;
5362
}
5463

@@ -57,76 +66,78 @@ export class EvmProvider extends BaseProvider {
5766
}
5867

5968
async getNetworkIdentifier(): Promise<string> {
60-
const result = await this.provider.getNetwork();
61-
return `evm_${result.chainId}`;
69+
const chainId = await this.client.getChainId();
70+
71+
return `evm_${chainId}`;
6272
}
6373

6474
async getLatestBlockNumber(): Promise<number> {
65-
return this.provider.getBlockNumber();
75+
const blockNumber = await this.client.getBlockNumber();
76+
77+
return Number(blockNumber);
6678
}
6779

6880
async getBlockHash(blockNumber: number) {
69-
const block = await this.provider.getBlock(blockNumber);
81+
const block = await this.client.getBlock({
82+
blockNumber: BigInt(blockNumber)
83+
});
84+
7085
return block.hash;
7186
}
7287

73-
async processBlock(blockNum: number, parentHash: string | null) {
88+
async processBlock(blockNumber: number, parentHash: string | null) {
7489
let block: Block | null = null;
7590
let eventsData: EventsData;
7691

7792
const skipBlockFetching = this.instance.opts?.skipBlockFetching ?? false;
7893
const hasPreloadedBlockEvents =
79-
skipBlockFetching && this.logsCache.has(blockNum);
94+
skipBlockFetching && this.logsCache.has(BigInt(blockNumber));
8095

8196
try {
8297
if (!hasPreloadedBlockEvents) {
83-
block = await this.provider.getBlock(blockNum);
98+
block = await this.client.getBlock({
99+
blockNumber: BigInt(blockNumber)
100+
});
84101
}
85-
} catch (e) {
86-
this.log.error(
87-
{ blockNumber: blockNum, err: e },
88-
'getting block failed... retrying'
89-
);
90-
throw e;
102+
} catch (err) {
103+
this.log.error({ blockNumber, err }, 'getting block failed... retrying');
104+
throw err;
91105
}
92106

93107
if (!hasPreloadedBlockEvents && block === null) {
94-
this.log.info({ blockNumber: blockNum }, 'block not found');
108+
this.log.info({ blockNumber }, 'block not found');
95109
throw new BlockNotFoundError();
96110
}
97111

98112
try {
99113
eventsData = await this.getEvents({
100-
blockNumber: blockNum,
114+
blockNumber: BigInt(blockNumber),
101115
blockHash: block?.hash ?? null
102116
});
103-
} catch (e: unknown) {
104-
if (e instanceof CustomJsonRpcError && e.code === -32000) {
105-
this.log.info({ blockNumber: blockNum }, 'block events not found');
117+
} catch (err: unknown) {
118+
if (err instanceof CustomJsonRpcError && err.code === -32000) {
119+
this.log.info({ blockNumber }, 'block events not found');
106120
throw new BlockNotFoundError();
107121
}
108122

109-
this.log.error(
110-
{ blockNumber: blockNum, err: e },
111-
'getting events failed... retrying'
112-
);
113-
throw e;
123+
this.log.error({ blockNumber, err }, 'getting events failed... retrying');
124+
throw err;
114125
}
115126

116127
if (block && parentHash && block.parentHash !== parentHash) {
117-
this.log.error({ blockNumber: blockNum }, 'reorg detected');
128+
this.log.error({ blockNumber }, 'reorg detected');
118129
throw new ReorgDetectedError();
119130
}
120131

121-
await this.handleBlock(blockNum, block, eventsData);
132+
await this.handleBlock(blockNumber, block, eventsData);
122133

123134
if (block) {
124-
await this.instance.setBlockHash(blockNum, block.hash);
135+
await this.instance.setBlockHash(blockNumber, block.hash);
125136
}
126137

127-
await this.instance.setLastIndexedBlock(blockNum);
138+
await this.instance.setLastIndexedBlock(blockNumber);
128139

129-
return blockNum + 1;
140+
return blockNumber + 1;
130141
}
131142

132143
private async handleBlock(
@@ -160,7 +171,7 @@ export class EvmProvider extends BaseProvider {
160171
) {
161172
this.log.debug({ txId }, 'handling transaction');
162173

163-
const helpers = await this.instance.getWriterHelpers();
174+
const helpers = this.instance.getWriterHelpers();
164175

165176
if (this.instance.config.tx_fn) {
166177
await this.writers[this.instance.config.tx_fn]({
@@ -184,7 +195,10 @@ export class EvmProvider extends BaseProvider {
184195
);
185196

186197
for (const event of logs) {
187-
const handler = globalEventHandlers[event.topics[0]];
198+
const eventHash = event.topics[0];
199+
if (!eventHash) continue;
200+
201+
const handler = globalEventHandlers[eventHash];
188202
if (!handler) continue;
189203

190204
this.log.info(
@@ -228,11 +242,15 @@ export class EvmProvider extends BaseProvider {
228242
'found contract event'
229243
);
230244

231-
let parsedEvent: LogDescription | undefined;
245+
let parsedEvent: ParseEventLogsReturnType[number] | undefined;
232246
if (source.abi && this.abis?.[source.abi]) {
233-
const iface = new Interface(this.abis[source.abi]);
234247
try {
235-
parsedEvent = iface.parseLog(log);
248+
const parsedLogs = parseEventLogs({
249+
abi: this.abis[source.abi],
250+
logs: [log]
251+
});
252+
parsedEvent =
253+
parsedLogs[0] as ParseEventLogsReturnType[number];
236254
} catch (err) {
237255
this.log.warn(
238256
{
@@ -298,7 +316,7 @@ export class EvmProvider extends BaseProvider {
298316
blockNumber
299317
}: {
300318
blockHash: string | null;
301-
blockNumber: number;
319+
blockNumber: bigint;
302320
}): Promise<EventsData> {
303321
let isPreloaded = false;
304322
let events: Log[] = [];
@@ -320,6 +338,8 @@ export class EvmProvider extends BaseProvider {
320338
return {
321339
isPreloaded,
322340
events: events.reduce((acc, event) => {
341+
if (event.transactionHash === null) return acc;
342+
323343
if (!acc[event.transactionHash]) acc[event.transactionHash] = [];
324344

325345
acc[event.transactionHash] = acc[event.transactionHash].concat(event);
@@ -350,7 +370,10 @@ export class EvmProvider extends BaseProvider {
350370
topics?: (string | string[])[];
351371
} = {};
352372

373+
let signal: AbortSignal | undefined;
374+
353375
if ('blockHash' in filter) {
376+
signal = AbortSignal.timeout(CLIENT_TIMEOUT);
354377
params.blockHash = filter.blockHash;
355378
}
356379

@@ -372,6 +395,7 @@ export class EvmProvider extends BaseProvider {
372395

373396
const res = await fetch(this.instance.config.network_node_url, {
374397
method: 'POST',
398+
signal,
375399
headers: {
376400
'Content-Type': 'application/json'
377401
},
@@ -397,9 +421,7 @@ export class EvmProvider extends BaseProvider {
397421
);
398422
}
399423

400-
return Formatter.arrayOf(this.formatter.filterLog.bind(this.formatter))(
401-
json.result
402-
);
424+
return json.result.map((log: RpcLog) => formatLog(log));
403425
}
404426

405427
async getLogs(
@@ -496,6 +518,8 @@ export class EvmProvider extends BaseProvider {
496518
});
497519

498520
for (const log of events) {
521+
if (log.blockNumber === null) continue;
522+
499523
if (!this.logsCache.has(log.blockNumber)) {
500524
this.logsCache.set(log.blockNumber, []);
501525
}
@@ -504,14 +528,14 @@ export class EvmProvider extends BaseProvider {
504528
}
505529

506530
return events.map(log => ({
507-
blockNumber: log.blockNumber,
531+
blockNumber: Number(log.blockNumber),
508532
contractAddress: log.address
509533
}));
510534
}
511535

512536
getEventHash(eventName: string) {
513537
if (!this.sourceHashes.has(eventName)) {
514-
this.sourceHashes.set(eventName, keccak256(toUtf8Bytes(eventName)));
538+
this.sourceHashes.set(eventName, keccak256(stringToBytes(eventName)));
515539
}
516540

517541
return this.sourceHashes.get(eventName) as string;

src/providers/evm/types.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
import { LogDescription } from '@ethersproject/abi';
2-
import { Log, Provider } from '@ethersproject/providers';
1+
import {
2+
Abi,
3+
ContractEventName,
4+
GetBlockReturnType,
5+
Log,
6+
ParseEventLogsReturnType
7+
} from 'viem';
38
import { BaseWriterParams } from '../../types';
49

510
export class CustomJsonRpcError extends Error {
@@ -22,13 +27,16 @@ export type EventsData = {
2227
events: Record<string, Log[]>;
2328
};
2429

25-
export type Block = Awaited<ReturnType<Provider['getBlock']>>;
30+
export type Block = GetBlockReturnType;
2631

27-
export type Writer = (
32+
export type Writer<
33+
WriterAbi extends Abi = any,
34+
EventName extends ContractEventName<WriterAbi> = any
35+
> = (
2836
args: {
2937
txId: string;
3038
block: Block | null;
3139
rawEvent?: Log;
32-
event?: LogDescription;
40+
event?: ParseEventLogsReturnType<WriterAbi, EventName, true>[number];
3341
} & BaseWriterParams
3442
) => Promise<void>;

0 commit comments

Comments
 (0)