@apibara/protocol 2.1.0-beta.41 → 2.1.0-beta.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +1 -0
- package/dist/index.mjs.map +1 -1
- package/dist/rpc/index.cjs +12 -0
- package/dist/rpc/index.cjs.map +1 -0
- package/dist/rpc/index.d.cts +6 -0
- package/dist/rpc/index.d.mts +6 -0
- package/dist/rpc/index.d.ts +6 -0
- package/dist/rpc/index.mjs +3 -0
- package/dist/rpc/index.mjs.map +1 -0
- package/dist/shared/protocol.6a091343.d.mts +102 -0
- package/dist/shared/protocol.91a69be4.cjs +518 -0
- package/dist/shared/protocol.91a69be4.cjs.map +1 -0
- package/dist/shared/protocol.aec6eac7.d.ts +102 -0
- package/dist/shared/protocol.d171ebd2.d.cts +102 -0
- package/dist/shared/protocol.ea189418.mjs +512 -0
- package/dist/shared/protocol.ea189418.mjs.map +1 -0
- package/package.json +7 -1
- package/src/index.ts +2 -0
- package/src/rpc/chain-tracker.ts +310 -0
- package/src/rpc/client.ts +51 -0
- package/src/rpc/config.ts +86 -0
- package/src/rpc/data-stream.ts +348 -0
- package/src/rpc/helpers.ts +9 -0
- package/src/rpc/index.ts +13 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { fromHex } from "viem";
|
|
2
|
+
import type { Bytes, Cursor } from "../common";
|
|
3
|
+
import type { BlockInfo } from "./config";
|
|
4
|
+
import { blockInfoToCursor } from "./helpers";
|
|
5
|
+
|
|
6
|
+
type UpdateHeadArgs = {
|
|
7
|
+
newHead: BlockInfo;
|
|
8
|
+
fetchCursorByHash: (hash: Bytes) => Promise<BlockInfo | null>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type UpdateHeadResult =
|
|
12
|
+
| {
|
|
13
|
+
status: "unchanged";
|
|
14
|
+
}
|
|
15
|
+
| {
|
|
16
|
+
status: "success";
|
|
17
|
+
}
|
|
18
|
+
| {
|
|
19
|
+
status: "reorg";
|
|
20
|
+
cursor: Cursor;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export class ChainTracker {
|
|
24
|
+
#finalized: BlockInfo;
|
|
25
|
+
#head: BlockInfo;
|
|
26
|
+
#canonical: Map<bigint, BlockInfo>;
|
|
27
|
+
|
|
28
|
+
constructor({ head, finalized }: { finalized: BlockInfo; head: BlockInfo }) {
|
|
29
|
+
this.#finalized = finalized;
|
|
30
|
+
this.#head = head;
|
|
31
|
+
this.#canonical = new Map([
|
|
32
|
+
[finalized.blockNumber, finalized],
|
|
33
|
+
[head.blockNumber, head],
|
|
34
|
+
]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
head(): Cursor {
|
|
38
|
+
return blockInfoToCursor(this.#head);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
finalized(): Cursor {
|
|
42
|
+
return blockInfoToCursor(this.#finalized);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
updateFinalized(newFinalized: BlockInfo) {
|
|
46
|
+
if (newFinalized.blockNumber < this.#finalized.blockNumber) {
|
|
47
|
+
throw new Error("Finalized cursor moved backwards");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (newFinalized.blockNumber === this.#finalized.blockNumber) {
|
|
51
|
+
if (newFinalized.blockHash !== this.#finalized.blockHash) {
|
|
52
|
+
throw new Error("Received a different finalized cursor");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Delete all blocks that are now finalized.
|
|
59
|
+
for (
|
|
60
|
+
let bn = this.#finalized.blockNumber;
|
|
61
|
+
bn < newFinalized.blockNumber;
|
|
62
|
+
bn++
|
|
63
|
+
) {
|
|
64
|
+
this.#canonical.delete(bn);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
this.#canonical.set(newFinalized.blockNumber, newFinalized);
|
|
68
|
+
this.#finalized = newFinalized;
|
|
69
|
+
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
addToCanonicalChain(blockInfo: BlockInfo) {
|
|
74
|
+
const existing = this.#canonical.get(blockInfo.blockNumber);
|
|
75
|
+
|
|
76
|
+
if (existing) {
|
|
77
|
+
if (existing.blockHash !== blockInfo.blockHash) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Block already exists in canonical chain: previous ${existing.blockHash}, new ${blockInfo.blockHash}`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const parent = this.#canonical.get(blockInfo.blockNumber - 1n);
|
|
85
|
+
if (!parent) {
|
|
86
|
+
throw new Error("Parent block not found");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (parent.blockHash !== blockInfo.parentBlockHash) {
|
|
90
|
+
throw new Error("Parent block hash mismatch.");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this.#canonical.set(blockInfo.blockNumber, blockInfo);
|
|
94
|
+
|
|
95
|
+
// console.log("Canon updated: ", canonical);
|
|
96
|
+
|
|
97
|
+
return { status: "success" };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async updateHead({
|
|
101
|
+
newHead,
|
|
102
|
+
fetchCursorByHash,
|
|
103
|
+
}: UpdateHeadArgs): Promise<UpdateHeadResult> {
|
|
104
|
+
// No changes to the chain.
|
|
105
|
+
if (
|
|
106
|
+
newHead.blockNumber === this.#head.blockNumber &&
|
|
107
|
+
newHead.blockHash === this.#head.blockHash
|
|
108
|
+
) {
|
|
109
|
+
return { status: "unchanged" };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Most common case: the new head is the block after the current head.
|
|
113
|
+
if (
|
|
114
|
+
newHead.blockNumber === this.#head.blockNumber + 1n &&
|
|
115
|
+
newHead.parentBlockHash === this.#head.blockHash
|
|
116
|
+
) {
|
|
117
|
+
this.#canonical.set(newHead.blockNumber, newHead);
|
|
118
|
+
this.#head = newHead;
|
|
119
|
+
return { status: "success" };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// The new chain is not longer.
|
|
123
|
+
if (newHead.blockNumber <= this.#head.blockNumber) {
|
|
124
|
+
// console.log("head=", this.#head, "newhead=", newHead);
|
|
125
|
+
let currentNewHead = newHead;
|
|
126
|
+
// Delete all blocks from canonical chain after the new head.
|
|
127
|
+
for (
|
|
128
|
+
let bn = newHead.blockNumber + 1n;
|
|
129
|
+
bn <= this.#head.blockNumber;
|
|
130
|
+
bn++
|
|
131
|
+
) {
|
|
132
|
+
this.#canonical.delete(bn);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Check if the chain was simply shrunk to this block.
|
|
136
|
+
const existing = this.#canonical.get(currentNewHead.blockNumber);
|
|
137
|
+
if (existing && existing.blockHash === currentNewHead.blockHash) {
|
|
138
|
+
this.#head = existing;
|
|
139
|
+
return {
|
|
140
|
+
status: "reorg",
|
|
141
|
+
cursor: blockInfoToCursor(existing),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
while (currentNewHead.blockNumber > this.#finalized.blockNumber) {
|
|
146
|
+
this.#canonical.delete(currentNewHead.blockNumber);
|
|
147
|
+
|
|
148
|
+
const canonicalParent = this.#canonical.get(
|
|
149
|
+
currentNewHead.blockNumber - 1n,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (!canonicalParent) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
"Cannot reconcile new head with canonical chain: missing parent in canonical chain",
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// We found the common ancestor.
|
|
159
|
+
if (canonicalParent.blockHash === currentNewHead.parentBlockHash) {
|
|
160
|
+
this.#head = canonicalParent;
|
|
161
|
+
return {
|
|
162
|
+
status: "reorg",
|
|
163
|
+
cursor: blockInfoToCursor(canonicalParent),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const parent = await fetchCursorByHash(currentNewHead.parentBlockHash);
|
|
168
|
+
|
|
169
|
+
if (!parent) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
"Cannot reconcile new head with canonical chain: failed to fetch parent",
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
currentNewHead = parent;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
throw new Error("Cannot reconcile new head with canonical chain.");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// In all other cases we need to "join" the new head with the existing chain.
|
|
182
|
+
// The new chain is longer and we need the missing blocks.
|
|
183
|
+
// This may result in reorgs.
|
|
184
|
+
|
|
185
|
+
let current = newHead;
|
|
186
|
+
let reorgDetected = false;
|
|
187
|
+
const blocksToApply = [newHead];
|
|
188
|
+
|
|
189
|
+
while (true) {
|
|
190
|
+
const parent = await fetchCursorByHash(current.parentBlockHash);
|
|
191
|
+
|
|
192
|
+
if (!parent) {
|
|
193
|
+
throw new Error(
|
|
194
|
+
"Cannot reconcile new head with canonical chain: failed to fetch parent",
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (parent.blockNumber === this.#head.blockNumber) {
|
|
199
|
+
if (parent.blockHash === this.#head.blockHash) {
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const headParent = this.#canonical.get(this.#head.blockNumber - 1n);
|
|
204
|
+
if (!headParent) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
"Cannot reconcile new head with canonical chain: missing parent in canonical chain",
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Update current head.
|
|
211
|
+
this.#canonical.delete(this.#head.blockNumber);
|
|
212
|
+
this.#head = headParent;
|
|
213
|
+
reorgDetected = true;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
blocksToApply.push(parent);
|
|
217
|
+
current = parent;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
for (const block of blocksToApply.reverse()) {
|
|
221
|
+
this.#canonical.set(block.blockNumber, block);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const previousHead = this.#head;
|
|
225
|
+
this.#head = newHead;
|
|
226
|
+
|
|
227
|
+
if (reorgDetected) {
|
|
228
|
+
return {
|
|
229
|
+
status: "reorg",
|
|
230
|
+
cursor: blockInfoToCursor(previousHead),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return { status: "success" };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async initializeStartingCursor({
|
|
238
|
+
cursor,
|
|
239
|
+
fetchCursor,
|
|
240
|
+
}: {
|
|
241
|
+
cursor: Cursor;
|
|
242
|
+
fetchCursor: (blockNumber: bigint) => Promise<BlockInfo | null>;
|
|
243
|
+
}): Promise<
|
|
244
|
+
| { canonical: true; reason?: undefined; fullCursor: Cursor }
|
|
245
|
+
| { canonical: false; reason: string; fullCursor?: undefined }
|
|
246
|
+
> {
|
|
247
|
+
const head = this.head();
|
|
248
|
+
if (cursor.orderKey > head.orderKey) {
|
|
249
|
+
return { canonical: false, reason: "cursor is ahead of head" };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!cursor.uniqueKey) {
|
|
253
|
+
const fullInfo = await fetchCursor(cursor.orderKey);
|
|
254
|
+
|
|
255
|
+
if (fullInfo === null) {
|
|
256
|
+
throw new Error("Failed to initialize canonical cursor");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return { canonical: true, fullCursor: blockInfoToCursor(fullInfo) };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const expectedInfo = await fetchCursor(cursor.orderKey);
|
|
263
|
+
|
|
264
|
+
if (expectedInfo === null) {
|
|
265
|
+
return {
|
|
266
|
+
canonical: false,
|
|
267
|
+
reason: "expected block does not exist",
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const expectedCursor = blockInfoToCursor(expectedInfo);
|
|
272
|
+
|
|
273
|
+
// These two checks are redundant, but they are kept to avoid issues with bad config implementations.
|
|
274
|
+
if (!expectedCursor.uniqueKey) {
|
|
275
|
+
return {
|
|
276
|
+
canonical: false,
|
|
277
|
+
reason: "expected cursor has no unique key (hash)",
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (expectedCursor.orderKey !== cursor.orderKey) {
|
|
282
|
+
return {
|
|
283
|
+
canonical: false,
|
|
284
|
+
reason: "cursor order key does not match expected order key",
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (
|
|
289
|
+
fromHex(expectedCursor.uniqueKey, "bigint") !==
|
|
290
|
+
fromHex(cursor.uniqueKey, "bigint")
|
|
291
|
+
) {
|
|
292
|
+
return {
|
|
293
|
+
canonical: false,
|
|
294
|
+
reason: `cursor hash does not match expected hash: ${cursor.uniqueKey} !== ${expectedCursor.uniqueKey}`,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return { canonical: true, fullCursor: expectedCursor };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function createChainTracker({
|
|
303
|
+
head,
|
|
304
|
+
finalized,
|
|
305
|
+
}: {
|
|
306
|
+
head: BlockInfo;
|
|
307
|
+
finalized: BlockInfo;
|
|
308
|
+
}): ChainTracker {
|
|
309
|
+
return new ChainTracker({ finalized, head });
|
|
310
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Client, ClientCallOptions, StreamDataOptions } from "../client";
|
|
2
|
+
import type { Cursor } from "../common";
|
|
3
|
+
import type { StatusRequest, StatusResponse } from "../status";
|
|
4
|
+
import type { StreamDataRequest, StreamDataResponse } from "../stream";
|
|
5
|
+
import type { RpcStreamConfig } from "./config";
|
|
6
|
+
import { RpcDataStream } from "./data-stream";
|
|
7
|
+
import { blockInfoToCursor } from "./helpers";
|
|
8
|
+
|
|
9
|
+
export class RpcClient<TFilter, TBlock> implements Client<TFilter, TBlock> {
|
|
10
|
+
constructor(private config: RpcStreamConfig<TFilter, TBlock>) {}
|
|
11
|
+
|
|
12
|
+
async status(
|
|
13
|
+
_request?: StatusRequest,
|
|
14
|
+
_options?: ClientCallOptions,
|
|
15
|
+
): Promise<StatusResponse> {
|
|
16
|
+
const [currentHead, finalized] = await Promise.all([
|
|
17
|
+
this.config.fetchCursor({ blockTag: "latest" }),
|
|
18
|
+
this.config.fetchCursor({ blockTag: "finalized" }),
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const starting: Cursor = { orderKey: 0n };
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
currentHead: currentHead ? blockInfoToCursor(currentHead) : undefined,
|
|
25
|
+
lastIngested: currentHead ? blockInfoToCursor(currentHead) : undefined,
|
|
26
|
+
finalized: finalized ? blockInfoToCursor(finalized) : undefined,
|
|
27
|
+
starting,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
streamData(
|
|
32
|
+
request: StreamDataRequest<TFilter>,
|
|
33
|
+
options?: StreamDataOptions,
|
|
34
|
+
): AsyncIterable<StreamDataResponse<TBlock>> {
|
|
35
|
+
const index = 0;
|
|
36
|
+
for (const filter of request.filter) {
|
|
37
|
+
const { valid, error } = this.config.validateFilter(filter);
|
|
38
|
+
if (!valid) {
|
|
39
|
+
throw new Error(`Filter at position ${index} is invalid: ${error}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return new RpcDataStream(this.config, request, options);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createRpcClient<TFilter, TBlock>(
|
|
48
|
+
config: RpcStreamConfig<TFilter, TBlock>,
|
|
49
|
+
): RpcClient<TFilter, TBlock> {
|
|
50
|
+
return new RpcClient(config);
|
|
51
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { Bytes, Cursor } from "../common";
|
|
2
|
+
|
|
3
|
+
export type FetchBlockRangeArgs<TFilter> = {
|
|
4
|
+
startBlock: bigint;
|
|
5
|
+
finalizedBlock: bigint;
|
|
6
|
+
filter: TFilter;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type FetchBlockRangeResult<TBlock> = {
|
|
10
|
+
startBlock: bigint;
|
|
11
|
+
endBlock: bigint;
|
|
12
|
+
data: FetchBlockResult<TBlock>[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type FetchBlockResult<TBlock> = {
|
|
16
|
+
block: TBlock | null;
|
|
17
|
+
cursor: Cursor | undefined;
|
|
18
|
+
endCursor: Cursor;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type BlockInfo = {
|
|
22
|
+
blockNumber: bigint;
|
|
23
|
+
blockHash: Bytes;
|
|
24
|
+
parentBlockHash: Bytes;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type FetchBlockByNumberArgs<TFilter> = {
|
|
28
|
+
blockNumber: bigint;
|
|
29
|
+
expectedParentBlockHash: Bytes;
|
|
30
|
+
filter: TFilter;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type FetchBlockByNumberResult<TBlock> =
|
|
34
|
+
| {
|
|
35
|
+
status: "success";
|
|
36
|
+
data: FetchBlockResult<TBlock>;
|
|
37
|
+
blockInfo: BlockInfo;
|
|
38
|
+
}
|
|
39
|
+
| {
|
|
40
|
+
status: "reorg";
|
|
41
|
+
blockInfo: BlockInfo;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type FetchCursorArgs =
|
|
45
|
+
| {
|
|
46
|
+
blockTag: "latest" | "finalized";
|
|
47
|
+
blockNumber?: undefined;
|
|
48
|
+
blockHash?: undefined;
|
|
49
|
+
}
|
|
50
|
+
| {
|
|
51
|
+
blockTag?: undefined;
|
|
52
|
+
blockNumber: bigint;
|
|
53
|
+
blockHash?: undefined;
|
|
54
|
+
}
|
|
55
|
+
| {
|
|
56
|
+
blockTag?: undefined;
|
|
57
|
+
blockNumber?: undefined;
|
|
58
|
+
blockHash: Bytes;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type ValidateFilterResult =
|
|
62
|
+
| {
|
|
63
|
+
valid: true;
|
|
64
|
+
error?: undefined;
|
|
65
|
+
}
|
|
66
|
+
| {
|
|
67
|
+
valid: false;
|
|
68
|
+
error: string;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export abstract class RpcStreamConfig<TFilter, TBlock> {
|
|
72
|
+
abstract headRefreshIntervalMs(): number;
|
|
73
|
+
abstract finalizedRefreshIntervalMs(): number;
|
|
74
|
+
|
|
75
|
+
abstract fetchCursor(args: FetchCursorArgs): Promise<BlockInfo | null>;
|
|
76
|
+
|
|
77
|
+
abstract validateFilter(filter: TFilter): ValidateFilterResult;
|
|
78
|
+
|
|
79
|
+
abstract fetchBlockRange(
|
|
80
|
+
args: FetchBlockRangeArgs<TFilter>,
|
|
81
|
+
): Promise<FetchBlockRangeResult<TBlock>>;
|
|
82
|
+
|
|
83
|
+
abstract fetchBlockByNumber(
|
|
84
|
+
args: FetchBlockByNumberArgs<TFilter>,
|
|
85
|
+
): Promise<FetchBlockByNumberResult<TBlock>>;
|
|
86
|
+
}
|