Skip to content

Commit a1a46fc

Browse files
committed
protocol: refactor the new RPC-based stream
1 parent c2fcc73 commit a1a46fc

28 files changed

Lines changed: 1431 additions & 1289 deletions

examples/evm-client/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
},
1313
"dependencies": {
1414
"@apibara/evm": "workspace:*",
15+
"@apibara/evm-rpc": "workspace:*",
1516
"@apibara/protocol": "workspace:*",
16-
"@apibara/rpc-evm": "workspace:*",
1717
"citty": "^0.1.6",
1818
"consola": "^3.4.2",
1919
"nice-grpc": "^2.1.8",
@@ -23,4 +23,4 @@
2323
"@types/node": "^20.12.12",
2424
"jiti": "^1.21.0"
2525
}
26-
}
26+
}

examples/evm-client/src/main-rpc.ts

Lines changed: 88 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import assert from "node:assert";
2-
import { type EvmRpcFilter, createEvmRpcClient } from "@apibara/rpc-evm";
2+
import { EvmRpcStream, type Filter } from "@apibara/evm-rpc";
3+
import { createRpcClient } from "@apibara/protocol/rpc";
34
import { defineCommand, runMain } from "citty";
45
import consola from "consola";
5-
import type { Chain } from "viem";
6+
import { http, type Chain, createPublicClient } from "viem";
67
import { mainnet, sepolia } from "viem/chains";
78

89
const command = defineCommand({
@@ -19,7 +20,7 @@ const command = defineCommand({
1920
},
2021
network: {
2122
type: "string",
22-
default: "sepolia",
23+
default: "mainnet",
2324
description: "Network name (mainnet, sepolia)",
2425
},
2526
contract: {
@@ -29,13 +30,13 @@ const command = defineCommand({
2930
},
3031
startBlock: {
3132
type: "string",
32-
default: "23857000",
33+
default: "23911400",
3334
description: "Starting block number",
3435
},
3536
finality: {
3637
type: "string",
3738
default: "finalized",
38-
description: "Block finality (finalized, accepted, pending)",
39+
description: "Block finality (finalized, accepted)",
3940
},
4041
},
4142
async run({ args }) {
@@ -44,42 +45,40 @@ const command = defineCommand({
4445

4546
const chain = (args.network === "mainnet" ? mainnet : sepolia) as Chain;
4647

47-
const client = createEvmRpcClient(args.rpcUrl, chain, {
48-
clientConfig: {
49-
retryDelay: 10000,
50-
onFetchRequest(request) {
51-
request
52-
.clone()
53-
.json()
54-
.then((body) => {
55-
if (Array.isArray(body)) {
56-
consola.debug(`----->>> Batched ${body.length} requests`);
57-
} else {
58-
consola.debug("----->>> Single request");
59-
}
60-
// console.dir(body, { depth: null });
61-
});
48+
const viemClient = createPublicClient({
49+
chain,
50+
transport: http(args.rpcUrl, {
51+
batch: {
52+
wait: 10,
6253
},
63-
},
54+
}),
6455
});
6556

66-
try {
67-
const status = await client.status();
68-
consola.success("Connected to RPC endpoint");
69-
consola.info("Current head:", status.currentHead?.orderKey);
70-
consola.info("Finalized:", status.finalized?.orderKey);
71-
} catch (error) {
72-
consola.error("Failed to connect:", error);
73-
process.exit(1);
74-
}
57+
const client = createRpcClient(
58+
new EvmRpcStream(viemClient, {
59+
getLogsRangeSize: 10n,
60+
}),
61+
);
62+
63+
const status = await client.status();
64+
consola.success("Connected to RPC endpoint");
65+
consola.info("Current head:", status.currentHead?.orderKey);
66+
consola.info("Finalized:", status.finalized?.orderKey);
7567

76-
const filter: EvmRpcFilter = {
68+
const filter: Filter = {
69+
header: "always",
7770
logs: [
7871
{
79-
address: args.contract as `0x${string}`,
80-
topics: [
81-
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" as `0x${string}`,
82-
],
72+
id: 1,
73+
address: "0xe0e0e08A6A4b9Dc7bD67BCB7aadE5cF48157d444",
74+
},
75+
{
76+
id: 2,
77+
address: "0xA37cc341634AFD9E0919D334606E676dbAb63E17",
78+
},
79+
{
80+
id: 3,
81+
address: "0xe0e0e08A6A4b9Dc7bD67BCB7aadE5cF48157d444",
8382
},
8483
],
8584
};
@@ -88,74 +87,70 @@ const command = defineCommand({
8887
consola.info("Monitoring contract:", args.contract);
8988
consola.info("Finality:", args.finality);
9089

91-
try {
92-
for await (const message of client.streamData({
93-
filter: [filter],
94-
finality: args.finality as "finalized" | "accepted" | "pending",
95-
startingCursor: {
96-
orderKey: BigInt(args.startBlock),
97-
},
98-
})) {
99-
switch (message._tag) {
100-
case "data": {
101-
const { data, endCursor, finality, production } = message.data;
102-
103-
for (const block of data) {
104-
assert(block !== null);
105-
const logs = block.logs;
106-
107-
if (logs.length > 0) {
108-
consola.info(
109-
`📦 Block ${block.header?.blockNumber} [${finality}/${production}]`,
110-
);
111-
consola.info(" Logs:", logs.length);
112-
113-
for (const log of logs) {
114-
consola.info(" 🔔 Log");
115-
consola.info(" Tx:", log.transactionHash);
116-
consola.info(" Address:", log.address);
117-
consola.info(" Topics:", log.topics.join(", "));
118-
consola.info(" Log Index:", log.logIndex);
119-
}
120-
}
121-
}
90+
for await (const message of client.streamData({
91+
filter: [filter],
92+
finality: args.finality as "finalized" | "accepted" | "pending",
93+
startingCursor: {
94+
orderKey: BigInt(args.startBlock),
95+
},
96+
})) {
97+
switch (message._tag) {
98+
case "data": {
99+
const { data, endCursor, finality, production } = message.data;
122100

123-
break;
124-
}
101+
for (const block of data) {
102+
assert(block !== null);
103+
const logs = block.logs;
125104

126-
case "invalidate": {
127-
consola.warn(
128-
"⚠️ Reorg detected! Invalidating to block",
129-
message.invalidate.cursor?.orderKey,
105+
consola.info(
106+
`block ${block.header?.blockNumber} [${finality}/${production}]`,
130107
);
131-
break;
108+
consola.info(" logs:", logs.length);
109+
110+
for (const log of logs) {
111+
// consola.info(log);
112+
// consola.info(" 🔔 Log");
113+
// consola.info(" Tx:", log.transactionHash);
114+
// consola.info(" Address:", log.address);
115+
// consola.info(" Topics:", log.topics.join(", "));
116+
// consola.info(" Log Index:", log.logIndex);
117+
}
132118
}
133119

134-
case "finalize": {
135-
consola.success(
136-
"✅ Finalized up to block",
137-
message.finalize.cursor?.orderKey,
138-
);
139-
break;
140-
}
120+
break;
121+
}
141122

142-
case "heartbeat": {
143-
consola.info("💓 Heartbeat");
144-
break;
145-
}
123+
case "invalidate": {
124+
consola.warn(
125+
"invalidating to block",
126+
message.invalidate.cursor?.orderKey,
127+
);
128+
break;
129+
}
146130

147-
case "systemMessage": {
148-
const output = message.systemMessage.output;
149-
if (output._tag === "stderr") {
150-
consola.error("System error:", output.stderr);
151-
}
152-
break;
131+
case "finalize": {
132+
consola.success(
133+
"finalized up to block",
134+
message.finalize.cursor?.orderKey,
135+
);
136+
break;
137+
}
138+
139+
case "heartbeat": {
140+
consola.info("heartbeat");
141+
break;
142+
}
143+
144+
case "systemMessage": {
145+
const output = message.systemMessage.output;
146+
if (output._tag === "stderr") {
147+
consola.error(output.stderr);
148+
} else {
149+
consola.info(output.stdout);
153150
}
151+
break;
154152
}
155153
}
156-
} catch (error) {
157-
consola.error("Stream error:", error);
158-
process.exit(1);
159154
}
160155
},
161156
});

packages/evm-rpc/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# `@apibara/evm-rpc`
2+
3+
This package provides a `EvmRpcStream` that implements the DNA stream using standard JSON-RPC endpoints.
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "@apibara/rpc-evm",
2+
"name": "@apibara/evm-rpc",
33
"version": "2.1.0-beta.40",
44
"type": "module",
55
"files": [
@@ -33,4 +33,4 @@
3333
"@apibara/evm": "workspace:*",
3434
"viem": "^2.13.2"
3535
}
36-
}
36+
}
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ import type {
33
Log as DnaLog,
44
} from "@apibara/evm";
55

6-
export type EvmRpcBlock = {
6+
export type BlockHeader = DnaBlockHeader;
7+
8+
export type Log = Omit<DnaLog, "logIndexInTransaction">;
9+
10+
export type Block = {
711
header: BlockHeader;
812
logs: Log[];
913
};
10-
11-
export type BlockHeader = DnaBlockHeader;
12-
13-
export type Log = DnaLog;

packages/evm-rpc/src/filter.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import type { LogFilter as DnaLogFilter, HeaderFilter } from "@apibara/evm";
2+
import type { ValidateFilterResult } from "@apibara/protocol/rpc";
3+
import { isHex } from "viem";
4+
5+
export type Filter = {
6+
header?: HeaderFilter;
7+
logs: LogFilter[];
8+
};
9+
10+
export type LogFilter = Pick<
11+
DnaLogFilter,
12+
"address" | "topics" | "strict" | "id"
13+
>;
14+
15+
export function validateFilter(filter: Filter): ValidateFilterResult {
16+
if (!filter.logs || filter.logs.length === 0) {
17+
return { valid: false, error: "Missing logs filter" };
18+
}
19+
20+
let logFilterIndex = 0;
21+
for (const logFilter of filter.logs ?? []) {
22+
if (
23+
logFilter.address === undefined &&
24+
(logFilter.topics?.length ?? 0) === 0
25+
) {
26+
return {
27+
valid: false,
28+
error: `Must provide at least one address or topic in log filter at position ${logFilterIndex}`,
29+
};
30+
}
31+
32+
if (logFilter.address) {
33+
if (!isHex(logFilter.address)) {
34+
return {
35+
valid: false,
36+
error: "Invalid address format. Expected 0x-prefixed hex string",
37+
};
38+
}
39+
}
40+
41+
if (logFilter.topics) {
42+
for (let i = 0; i < logFilter.topics.length; i++) {
43+
const topic = logFilter.topics[i];
44+
if (topic === null) {
45+
continue;
46+
}
47+
48+
if (!isHex(topic)) {
49+
return {
50+
valid: false,
51+
error: `Invalid topic at index ${i}: ${topic}. Must be null or a 0x-prefixed hex string`,
52+
};
53+
}
54+
}
55+
}
56+
57+
logFilterIndex++;
58+
}
59+
60+
return { valid: true };
61+
}

packages/evm-rpc/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { EvmRpcStream } from "./stream-config";
2+
export type { Filter, LogFilter } from "./filter";
3+
export type { Block, BlockHeader, Log } from "./block";

0 commit comments

Comments
 (0)