@effectstream/sync 0.100.0
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/package.json +29 -0
- package/src/mod.ts +3 -0
- package/src/sync-protocols/avail/AvailClient.ts +120 -0
- package/src/sync-protocols/avail/fetcher.ts +174 -0
- package/src/sync-protocols/avail/state.ts +134 -0
- package/src/sync-protocols/avail/types.ts +92 -0
- package/src/sync-protocols/base/fetcher.ts +43 -0
- package/src/sync-protocols/base/page.ts +89 -0
- package/src/sync-protocols/base/primitive.ts +24 -0
- package/src/sync-protocols/base/state.ts +143 -0
- package/src/sync-protocols/bitcoin/fetcher.ts +549 -0
- package/src/sync-protocols/bitcoin/state.ts +144 -0
- package/src/sync-protocols/bitcoin/types.ts +92 -0
- package/src/sync-protocols/celestia/CelestiaClient.ts +139 -0
- package/src/sync-protocols/celestia/fetcher.ts +165 -0
- package/src/sync-protocols/celestia/state.ts +131 -0
- package/src/sync-protocols/celestia/types.ts +46 -0
- package/src/sync-protocols/common/page-helpers.ts +65 -0
- package/src/sync-protocols/common/root.ts +35 -0
- package/src/sync-protocols/common/utils.ts +29 -0
- package/src/sync-protocols/evm/fetcher.ts +290 -0
- package/src/sync-protocols/evm/state.ts +122 -0
- package/src/sync-protocols/evm/types.ts +53 -0
- package/src/sync-protocols/midnight/MidnightClient.ts +210 -0
- package/src/sync-protocols/midnight/fetcher.ts +252 -0
- package/src/sync-protocols/midnight/state.ts +147 -0
- package/src/sync-protocols/midnight/types.ts +128 -0
- package/src/sync-protocols/midnight/zswap-decoder.ts +123 -0
- package/src/sync-protocols/mod.ts +29 -0
- package/src/sync-protocols/near/NearClient.ts +178 -0
- package/src/sync-protocols/near/fetcher.ts +274 -0
- package/src/sync-protocols/near/state.ts +130 -0
- package/src/sync-protocols/near/types.ts +46 -0
- package/src/sync-protocols/ntp/fetcher.ts +199 -0
- package/src/sync-protocols/ntp/state.ts +118 -0
- package/src/sync-protocols/ntp/types.ts +40 -0
- package/src/sync-protocols/orchestration/merge.ts +215 -0
- package/src/sync-protocols/orchestration/sync.ts +74 -0
- package/src/sync-protocols/types.ts +32 -0
- package/src/sync-protocols/utxorpc/BufferedRpc.ts +169 -0
- package/src/sync-protocols/utxorpc/fetcher.ts +116 -0
- package/src/sync-protocols/utxorpc/state.ts +186 -0
- package/src/sync-protocols/utxorpc/types.ts +53 -0
- package/src/sync-protocols/utxorpc/utils.ts +80 -0
- package/src/syncProtocolFactory.ts +149 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Operation } from "effection";
|
|
2
|
+
import type { PageRange } from "../base/page.ts";
|
|
3
|
+
import { narrowResult } from "@effectstream/utils";
|
|
4
|
+
import { ComponentNames, log, SeverityNumber } from "@effectstream/log";
|
|
5
|
+
import type { PaginatedSyncProtocols } from "../types.ts";
|
|
6
|
+
import type { PageTypeOf } from "../base/state.ts";
|
|
7
|
+
|
|
8
|
+
type ConfigSubset<Page> = {
|
|
9
|
+
name: string;
|
|
10
|
+
/**
|
|
11
|
+
* inclusive
|
|
12
|
+
*/
|
|
13
|
+
startPage: Page;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type PageSyncRange<Page> = PageRange<Page> & { isPresync: boolean };
|
|
17
|
+
|
|
18
|
+
export function* genInputRange<SyncProtocol extends PaginatedSyncProtocols>(
|
|
19
|
+
state: SyncProtocol,
|
|
20
|
+
genesisPage: PageTypeOf<SyncProtocol>,
|
|
21
|
+
config: ConfigSubset<PageTypeOf<SyncProtocol>>,
|
|
22
|
+
namespace: string[],
|
|
23
|
+
): Operation<undefined | PageSyncRange<PageTypeOf<SyncProtocol>>> {
|
|
24
|
+
const nextPage = state.lastPage == null
|
|
25
|
+
? genesisPage
|
|
26
|
+
: state.fetcher.nextInterval(state.lastPage.own).from;
|
|
27
|
+
// TODO This will always be false as the genesis page is the startPage
|
|
28
|
+
const isPresync = state.pageRelation.compare(nextPage, config.startPage) < 0;
|
|
29
|
+
|
|
30
|
+
const maxPage = isPresync
|
|
31
|
+
? state.fetcher.previousInterval(config.startPage).to
|
|
32
|
+
: yield* state.fetcher.getLatestPage(state.lastPage?.own);
|
|
33
|
+
const clampedMax = state.pageRelation.min(
|
|
34
|
+
maxPage,
|
|
35
|
+
state.fetcher.intervalFromStart(nextPage).to,
|
|
36
|
+
);
|
|
37
|
+
// this can happen if the genesis is not the exact start of the system
|
|
38
|
+
// and (esp. on local networks) the system hasn't reached the genesis page yet
|
|
39
|
+
if (state.pageRelation.compare(nextPage, clampedMax) > 0) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const pageRange = state.pageRelation.equals(clampedMax, nextPage)
|
|
44
|
+
? `[${nextPage}]`
|
|
45
|
+
: `[${nextPage}, ${clampedMax}]`;
|
|
46
|
+
log.remote(
|
|
47
|
+
ComponentNames.EFFECTSTREAM_SYNC,
|
|
48
|
+
namespace,
|
|
49
|
+
SeverityNumber.INFO,
|
|
50
|
+
(log) => log(pageRange),
|
|
51
|
+
);
|
|
52
|
+
return {
|
|
53
|
+
from: narrowResult<
|
|
54
|
+
PageTypeOf<PaginatedSyncProtocols>,
|
|
55
|
+
PageTypeOf<PaginatedSyncProtocols>,
|
|
56
|
+
PageTypeOf<SyncProtocol>
|
|
57
|
+
>(nextPage),
|
|
58
|
+
to: narrowResult<
|
|
59
|
+
PageTypeOf<PaginatedSyncProtocols>,
|
|
60
|
+
PageTypeOf<PaginatedSyncProtocols>,
|
|
61
|
+
PageTypeOf<SyncProtocol>
|
|
62
|
+
>(clampedMax),
|
|
63
|
+
isPresync,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BlockHash,
|
|
3
|
+
BlockNumber,
|
|
4
|
+
EffectstreamBlockNumber,
|
|
5
|
+
TimestampMs,
|
|
6
|
+
} from "@effectstream/utils";
|
|
7
|
+
import type { PageRelation } from "../base/page.ts";
|
|
8
|
+
import type {
|
|
9
|
+
ConfigSyncProtocolType,
|
|
10
|
+
FlattenSyncProtocolIOFor,
|
|
11
|
+
} from "@effectstream/config";
|
|
12
|
+
|
|
13
|
+
export type ChainPage = TimestampMs;
|
|
14
|
+
export type ChainBlock = {
|
|
15
|
+
blockNumber: EffectstreamBlockNumber;
|
|
16
|
+
timestamp: TimestampMs;
|
|
17
|
+
blockInfo: {
|
|
18
|
+
protocol_name: string;
|
|
19
|
+
block_number: BlockNumber;
|
|
20
|
+
blockHash: BlockHash;
|
|
21
|
+
}[];
|
|
22
|
+
primitives: (
|
|
23
|
+
& FlattenSyncProtocolIOFor<
|
|
24
|
+
ConfigSyncProtocolType
|
|
25
|
+
>
|
|
26
|
+
& { source: string }
|
|
27
|
+
)[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const chainPageRelation: PageRelation<ChainPage> = {
|
|
31
|
+
compare: (p1, p2) => p1 - p2,
|
|
32
|
+
equals: (p1, p2) => p1 === p2,
|
|
33
|
+
min: (p1, p2) => (p1 < p2 ? p1 : p2),
|
|
34
|
+
max: (p1, p2) => (p1 > p2 ? p1 : p2),
|
|
35
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AbsoluteSlotNumber,
|
|
3
|
+
BlockNumber,
|
|
4
|
+
IntervalMs,
|
|
5
|
+
TimestampMs,
|
|
6
|
+
} from "@effectstream/utils";
|
|
7
|
+
import type { PageRelation } from "../base/page.ts";
|
|
8
|
+
|
|
9
|
+
export function applyDelay(
|
|
10
|
+
timestamp: TimestampMs,
|
|
11
|
+
delayMs: IntervalMs,
|
|
12
|
+
): TimestampMs {
|
|
13
|
+
// note: plus ("+") here makes it look like this chain is further along than it is
|
|
14
|
+
// this is exactly what we want to make old blocks get merged into recent root data
|
|
15
|
+
return Math.max(0, timestamp + delayMs);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const blockNumberRelation: PageRelation<BlockNumber> = {
|
|
19
|
+
compare: (p1, p2) => p1 - p2,
|
|
20
|
+
equals: (p1, p2) => p1 === p2,
|
|
21
|
+
min: (p1, p2) => (p1 < p2 ? p1 : p2),
|
|
22
|
+
max: (p1, p2) => (p1 > p2 ? p1 : p2),
|
|
23
|
+
};
|
|
24
|
+
export const absSlotRelation: PageRelation<AbsoluteSlotNumber> = {
|
|
25
|
+
compare: (p1, p2) => p1 - p2,
|
|
26
|
+
equals: (p1, p2) => p1 === p2,
|
|
27
|
+
min: (p1, p2) => (p1 < p2 ? p1 : p2),
|
|
28
|
+
max: (p1, p2) => (p1 > p2 ? p1 : p2),
|
|
29
|
+
};
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseDataFetcher,
|
|
3
|
+
type DataFetched,
|
|
4
|
+
type PaginatedFetcher,
|
|
5
|
+
} from "../base/fetcher.ts";
|
|
6
|
+
import type { RootOutput, RootPage } from "../types.ts";
|
|
7
|
+
import type { AbiEvent, Chain, GetBlockReturnType, PublicClient } from "viem";
|
|
8
|
+
import type { Operation } from "effection";
|
|
9
|
+
import { all, call } from "effection";
|
|
10
|
+
import {
|
|
11
|
+
bound,
|
|
12
|
+
type EvmBlockHash,
|
|
13
|
+
type EvmRpcPageJson,
|
|
14
|
+
keysOf,
|
|
15
|
+
} from "@effectstream/utils";
|
|
16
|
+
import { blockNumberRelation } from "../common/utils.ts";
|
|
17
|
+
import type {
|
|
18
|
+
ConfigType,
|
|
19
|
+
Input,
|
|
20
|
+
Output,
|
|
21
|
+
Page,
|
|
22
|
+
PrimitiveEntryType,
|
|
23
|
+
PrimitiveType,
|
|
24
|
+
} from "./types.ts";
|
|
25
|
+
import { PageSchema } from "./types.ts";
|
|
26
|
+
import {
|
|
27
|
+
fetchNewestPage,
|
|
28
|
+
genImmediatePageRequests,
|
|
29
|
+
type PageRange,
|
|
30
|
+
type PageRequest,
|
|
31
|
+
} from "../base/page.ts";
|
|
32
|
+
import type { PrimitiveFetcher } from "../base/primitive.ts";
|
|
33
|
+
import type { RootConversion } from "../base/state.ts";
|
|
34
|
+
import type {
|
|
35
|
+
ConfigNetworkType,
|
|
36
|
+
PrimitiveEntry,
|
|
37
|
+
SyncProtocolWithNetwork,
|
|
38
|
+
} from "@effectstream/config";
|
|
39
|
+
import type { ConfigSyncProtocolType } from "@effectstream/config";
|
|
40
|
+
import { Value } from "@sinclair/typebox/value";
|
|
41
|
+
|
|
42
|
+
export class EvmFetcher
|
|
43
|
+
extends BaseDataFetcher<Input, Output, RootOutput, Page, RootPage>
|
|
44
|
+
implements
|
|
45
|
+
PrimitiveFetcher<
|
|
46
|
+
Input,
|
|
47
|
+
Page,
|
|
48
|
+
GetBlockReturnType<Chain>,
|
|
49
|
+
PrimitiveType,
|
|
50
|
+
ConfigSyncProtocolType.EVM_RPC_PARALLEL
|
|
51
|
+
>,
|
|
52
|
+
PaginatedFetcher<Page> {
|
|
53
|
+
constructor(
|
|
54
|
+
readonly config: ConfigType,
|
|
55
|
+
readonly client: PublicClient<any, Chain, any, any>,
|
|
56
|
+
) {
|
|
57
|
+
super(config.syncProtocol.name);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@bound
|
|
61
|
+
override *readData(
|
|
62
|
+
data: Input,
|
|
63
|
+
rootConversion: RootConversion<Output, RootOutput, RootPage>,
|
|
64
|
+
): Operation<DataFetched<Output, Page, RootPage>> {
|
|
65
|
+
const allPages: Page[] = [];
|
|
66
|
+
for (let p = data.from; p <= data.to; p++) allPages.push(p);
|
|
67
|
+
const pageFetcher = genImmediatePageRequests(
|
|
68
|
+
allPages,
|
|
69
|
+
(page) => this.client.getBlock({ blockNumber: BigInt(page) }),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// TODO: if we expect multiple primitives per block
|
|
73
|
+
// it can, depending on the chain, be faster to just download the full block and parse it locally
|
|
74
|
+
const primitives = yield* this.readPrimitives(
|
|
75
|
+
data,
|
|
76
|
+
pageFetcher,
|
|
77
|
+
this.config.primitives,
|
|
78
|
+
);
|
|
79
|
+
const groupedByPage = this.groupByPage(primitives);
|
|
80
|
+
|
|
81
|
+
const [output, lastPage] = yield* all(
|
|
82
|
+
[
|
|
83
|
+
all(
|
|
84
|
+
keysOf(groupedByPage).map(function* (pageJson) {
|
|
85
|
+
const page = Value.Encode(PageSchema, pageJson);
|
|
86
|
+
const raw = yield* call(() => pageFetcher(page));
|
|
87
|
+
const blockHashes = [raw.hash] as EvmBlockHash[];
|
|
88
|
+
return {
|
|
89
|
+
raw,
|
|
90
|
+
primitives: groupedByPage[pageJson],
|
|
91
|
+
blockHashes,
|
|
92
|
+
};
|
|
93
|
+
}),
|
|
94
|
+
),
|
|
95
|
+
// we always need to get the last page in the parallel case
|
|
96
|
+
// so we can use it for pagination
|
|
97
|
+
call(() => pageFetcher(Number(data.to))),
|
|
98
|
+
],
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// Build an output with all page info, as we need all the hashes to build the effectstream-block-hash
|
|
102
|
+
// All blocks are already pre-fetched in parallel by genImmediatePageRequests above
|
|
103
|
+
const allOutputs: Output[] = [];
|
|
104
|
+
for (let page = data.from; page <= data.to; page++) {
|
|
105
|
+
const _output = output.find((o) => o.raw.number === BigInt(page));
|
|
106
|
+
if (_output) {
|
|
107
|
+
allOutputs.push(_output);
|
|
108
|
+
} else {
|
|
109
|
+
const raw = yield* call(() => pageFetcher(page));
|
|
110
|
+
allOutputs.push({
|
|
111
|
+
raw,
|
|
112
|
+
primitives: [],
|
|
113
|
+
blockHashes: [raw.hash] as EvmBlockHash[],
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
output: allOutputs.map((o) => ({
|
|
120
|
+
output: o,
|
|
121
|
+
cleanup: () => {}, // no cleanup required
|
|
122
|
+
})),
|
|
123
|
+
lastPage: {
|
|
124
|
+
own: Number(data.to),
|
|
125
|
+
ownBlockNumber: Number(data.to),
|
|
126
|
+
root: rootConversion.toRootPage({
|
|
127
|
+
blockHashes: [lastPage.hash] as EvmBlockHash[],
|
|
128
|
+
primitives: [], // unused in toRootPage
|
|
129
|
+
raw: lastPage,
|
|
130
|
+
}),
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@bound
|
|
136
|
+
groupByPage(
|
|
137
|
+
primitives: PrimitiveType[],
|
|
138
|
+
): Record<EvmRpcPageJson, PrimitiveType[]> {
|
|
139
|
+
return primitives.reduce((acc, primitive) => {
|
|
140
|
+
const key = Value.Decode(
|
|
141
|
+
PageSchema,
|
|
142
|
+
Number(primitive.syncProtocol.blockNumber),
|
|
143
|
+
);
|
|
144
|
+
(acc[key] ??= []).push(
|
|
145
|
+
primitive,
|
|
146
|
+
);
|
|
147
|
+
return acc;
|
|
148
|
+
}, {} as Record<EvmRpcPageJson, PrimitiveType[]>);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
fetchLogsAndExtractPrimitiveData = function* (
|
|
152
|
+
fromBlock: bigint,
|
|
153
|
+
toBlock: bigint,
|
|
154
|
+
client: PublicClient<any, Chain, any, any>,
|
|
155
|
+
primitiveEntry: PrimitiveEntryType,
|
|
156
|
+
abi: AbiEvent,
|
|
157
|
+
pageRequest: PageRequest<Page, GetBlockReturnType<Chain>>,
|
|
158
|
+
): Operation<
|
|
159
|
+
PrimitiveType[]
|
|
160
|
+
> {
|
|
161
|
+
// Fetch range of event-logs
|
|
162
|
+
const logs = yield* call(() =>
|
|
163
|
+
client.getLogs({
|
|
164
|
+
address: primitiveEntry.primitive.contractAddress as `0x${string}`,
|
|
165
|
+
event: abi,
|
|
166
|
+
fromBlock,
|
|
167
|
+
toBlock,
|
|
168
|
+
strict: true,
|
|
169
|
+
})
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// Create PrimitiveType for each event.
|
|
173
|
+
const primitiveResponses: PrimitiveType[] = [];
|
|
174
|
+
for (const log of logs) {
|
|
175
|
+
// const block = yield* call(() => pageRequest(Number(log.blockNumber)));
|
|
176
|
+
const args = (log as unknown as {
|
|
177
|
+
args: Record<string, any>;
|
|
178
|
+
}).args;
|
|
179
|
+
const primitiveResponse: PrimitiveType = {
|
|
180
|
+
syncProtocol: {
|
|
181
|
+
name: primitiveEntry.syncProtocol,
|
|
182
|
+
blockNumber: Number(log.blockNumber),
|
|
183
|
+
transactionHash: log.transactionHash,
|
|
184
|
+
transactionIndex: log.transactionIndex,
|
|
185
|
+
contractAddress: log.address,
|
|
186
|
+
logIndex: log.logIndex,
|
|
187
|
+
},
|
|
188
|
+
primitive: primitiveEntry.primitive.name,
|
|
189
|
+
output: {
|
|
190
|
+
payloadType: abi.name,
|
|
191
|
+
payload: args,
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
primitiveResponses.push(primitiveResponse);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return primitiveResponses;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
@bound
|
|
202
|
+
*readPrimitives(
|
|
203
|
+
data: Input,
|
|
204
|
+
pageRequest: PageRequest<Page, GetBlockReturnType<Chain>>,
|
|
205
|
+
primitiveEntries: Extract<
|
|
206
|
+
PrimitiveEntry,
|
|
207
|
+
{ syncProtocol: ConfigSyncProtocolType.EVM_RPC_PARALLEL }
|
|
208
|
+
>[],
|
|
209
|
+
): Operation<PrimitiveType[]> {
|
|
210
|
+
const client = this.client;
|
|
211
|
+
const allOperations: Operation<PrimitiveType[]>[] = [];
|
|
212
|
+
|
|
213
|
+
const fromBlock = yield* call(() => pageRequest(data.from));
|
|
214
|
+
const toBlock = yield* call(() => pageRequest(data.to));
|
|
215
|
+
for (const primitive of primitiveEntries) {
|
|
216
|
+
if (Array.isArray(primitive.primitive.abi)) {
|
|
217
|
+
const abis = primitive.primitive.abi as AbiEvent[];
|
|
218
|
+
for (const abi of abis) {
|
|
219
|
+
allOperations.push(
|
|
220
|
+
this.fetchLogsAndExtractPrimitiveData(
|
|
221
|
+
fromBlock.number,
|
|
222
|
+
toBlock.number,
|
|
223
|
+
client,
|
|
224
|
+
primitive,
|
|
225
|
+
abi,
|
|
226
|
+
pageRequest,
|
|
227
|
+
),
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
allOperations.push(
|
|
232
|
+
this.fetchLogsAndExtractPrimitiveData(
|
|
233
|
+
fromBlock.number,
|
|
234
|
+
toBlock.number,
|
|
235
|
+
client,
|
|
236
|
+
primitive,
|
|
237
|
+
primitive.primitive.abi as AbiEvent,
|
|
238
|
+
pageRequest,
|
|
239
|
+
),
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return (yield* all(allOperations)).flat();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
@bound
|
|
248
|
+
*getLatestPage(
|
|
249
|
+
knownLastPage: undefined | Page,
|
|
250
|
+
): Operation<Page> {
|
|
251
|
+
return yield* fetchNewestPage<Page>(
|
|
252
|
+
blockNumberRelation,
|
|
253
|
+
// override Viem's cache logic with out own
|
|
254
|
+
() =>
|
|
255
|
+
call(() =>
|
|
256
|
+
this.client.getBlockNumber({ cacheTime: 0 }).then(Number).then(
|
|
257
|
+
(height) => {
|
|
258
|
+
const config = this.config.syncProtocol;
|
|
259
|
+
if ("confirmationDepth" in config) {
|
|
260
|
+
return height - config.confirmationDepth;
|
|
261
|
+
}
|
|
262
|
+
return height;
|
|
263
|
+
},
|
|
264
|
+
)
|
|
265
|
+
),
|
|
266
|
+
knownLastPage,
|
|
267
|
+
this.config.syncProtocol.pollingInterval,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
@bound
|
|
272
|
+
previousInterval(nextIntervalStart: Page): PageRange<Page> {
|
|
273
|
+
return this.intervalFromStart(
|
|
274
|
+
nextIntervalStart - this.config.syncProtocol.stepSize - 1,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
@bound
|
|
279
|
+
nextInterval(prevIntervalEnd: Page): PageRange<Page> {
|
|
280
|
+
return this.intervalFromStart(prevIntervalEnd + 1);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
@bound
|
|
284
|
+
intervalFromStart(start: Page): PageRange<Page> {
|
|
285
|
+
return {
|
|
286
|
+
from: start,
|
|
287
|
+
to: start + this.config.syncProtocol.stepSize,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { applyDelay, blockNumberRelation } from "../common/utils.ts";
|
|
2
|
+
import type { Operation } from "effection";
|
|
3
|
+
import { call } from "effection";
|
|
4
|
+
import { getPage } from "@effectstream/db";
|
|
5
|
+
import type { PoolClient } from "pg";
|
|
6
|
+
import { bound, type EvmBlockNumber } from "@effectstream/utils";
|
|
7
|
+
import { type LastPage, SyncState } from "../base/state.ts";
|
|
8
|
+
import type { RootOutput, RootPage } from "../types.ts";
|
|
9
|
+
import type { EvmFetcher } from "./fetcher.ts";
|
|
10
|
+
import { genInputRange } from "../common/page-helpers.ts";
|
|
11
|
+
import type { Input, Output, Page } from "./types.ts";
|
|
12
|
+
import { toMsTimestamp } from "./types.ts";
|
|
13
|
+
import type { ConfigNetworkType, SyncProtocolWithNetwork } from "@effectstream/config";
|
|
14
|
+
|
|
15
|
+
export class EvmSyncState extends SyncState<
|
|
16
|
+
Input,
|
|
17
|
+
Output,
|
|
18
|
+
Page,
|
|
19
|
+
RootOutput,
|
|
20
|
+
RootPage,
|
|
21
|
+
EvmFetcher
|
|
22
|
+
> {
|
|
23
|
+
constructor(
|
|
24
|
+
lastPage: LastPage<Page, RootPage>,
|
|
25
|
+
readonly config: Extract<
|
|
26
|
+
SyncProtocolWithNetwork,
|
|
27
|
+
{ networkType: ConfigNetworkType.EVM }
|
|
28
|
+
>,
|
|
29
|
+
fetcher: EvmFetcher,
|
|
30
|
+
dbConn: PoolClient,
|
|
31
|
+
) {
|
|
32
|
+
super(
|
|
33
|
+
config.syncProtocol.name,
|
|
34
|
+
lastPage,
|
|
35
|
+
fetcher,
|
|
36
|
+
blockNumberRelation,
|
|
37
|
+
dbConn,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@bound
|
|
42
|
+
override toRootOutput(data: Output): RootOutput {
|
|
43
|
+
throw new Error("Only main chains create root outputs");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@bound
|
|
47
|
+
override toRootPage(data: Output): RootPage {
|
|
48
|
+
return applyDelay(
|
|
49
|
+
toMsTimestamp(data.raw.timestamp),
|
|
50
|
+
this.config.syncProtocol.delayMs,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@bound
|
|
55
|
+
override *stateToInput(): Operation<Input | undefined> {
|
|
56
|
+
return yield* genInputRange(
|
|
57
|
+
this as EvmSyncState,
|
|
58
|
+
this.config.syncProtocol.startBlockHeight as EvmBlockNumber,
|
|
59
|
+
{
|
|
60
|
+
name: this.config.syncProtocol.name,
|
|
61
|
+
startPage: this.config.syncProtocol.startBlockHeight as EvmBlockNumber,
|
|
62
|
+
},
|
|
63
|
+
this.getNamespace(),
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@bound
|
|
68
|
+
override toPage(
|
|
69
|
+
input: Input,
|
|
70
|
+
data: Output[],
|
|
71
|
+
): Page {
|
|
72
|
+
return Number(input.to);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@bound
|
|
76
|
+
override mergeDatum(
|
|
77
|
+
ourOutput: Output,
|
|
78
|
+
rootOutput: RootOutput,
|
|
79
|
+
): void {
|
|
80
|
+
const primitives = ourOutput.primitives.map((p) => ({
|
|
81
|
+
...p,
|
|
82
|
+
source: this.config.syncProtocol.name,
|
|
83
|
+
}));
|
|
84
|
+
const blockInfo = ourOutput.blockHashes.map((h) => ({
|
|
85
|
+
protocol_name: this.config.syncProtocol.name,
|
|
86
|
+
block_number: Number(ourOutput.raw.number),
|
|
87
|
+
blockHash: h,
|
|
88
|
+
}));
|
|
89
|
+
rootOutput.primitives.push(...primitives);
|
|
90
|
+
rootOutput.blockInfo.push(...blockInfo);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@bound
|
|
94
|
+
override getNamespace(): string[] {
|
|
95
|
+
return [this.config.network.name, this.config.syncProtocol.name];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
static *restoreState(
|
|
99
|
+
dbConn: PoolClient,
|
|
100
|
+
config: Extract<
|
|
101
|
+
SyncProtocolWithNetwork,
|
|
102
|
+
{ networkType: ConfigNetworkType.EVM }
|
|
103
|
+
>,
|
|
104
|
+
fetcher: EvmFetcher,
|
|
105
|
+
): Operation<EvmSyncState> {
|
|
106
|
+
// TODO: move this DB query into page-helpers?
|
|
107
|
+
const [result] = yield* call(async () =>
|
|
108
|
+
await getPage.run({
|
|
109
|
+
protocol_name: config.syncProtocol.name,
|
|
110
|
+
}, dbConn)
|
|
111
|
+
);
|
|
112
|
+
const page: any = result
|
|
113
|
+
? result.page as unknown as LastPage<Page, RootPage>
|
|
114
|
+
: undefined;
|
|
115
|
+
return new EvmSyncState(
|
|
116
|
+
page,
|
|
117
|
+
config,
|
|
118
|
+
fetcher,
|
|
119
|
+
dbConn,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BlockNumber,
|
|
3
|
+
EvmBlockHash,
|
|
4
|
+
EvmRpcPageJson,
|
|
5
|
+
TimestampMs,
|
|
6
|
+
} from "@effectstream/utils";
|
|
7
|
+
import { TypeboxHelpers } from "@effectstream/utils";
|
|
8
|
+
import type { Chain, GetBlockReturnType } from "viem";
|
|
9
|
+
import type { PageSyncRange } from "../common/page-helpers.ts";
|
|
10
|
+
import { Type } from "@sinclair/typebox";
|
|
11
|
+
import type {
|
|
12
|
+
ConfigNetworkType,
|
|
13
|
+
ConfigSyncProtocolType,
|
|
14
|
+
FlattenSyncProtocolIOFor,
|
|
15
|
+
PrimitiveEntry,
|
|
16
|
+
SyncProtocolWithNetwork,
|
|
17
|
+
} from "@effectstream/config";
|
|
18
|
+
|
|
19
|
+
export type Page = BlockNumber;
|
|
20
|
+
const PageJsonSchema = Type.Unsafe<
|
|
21
|
+
EvmRpcPageJson
|
|
22
|
+
>(Type.String());
|
|
23
|
+
export const PageSchema = TypeboxHelpers.SerializeObjAsJson<
|
|
24
|
+
Page,
|
|
25
|
+
typeof PageJsonSchema
|
|
26
|
+
>();
|
|
27
|
+
|
|
28
|
+
export type ConfigType = Extract<
|
|
29
|
+
SyncProtocolWithNetwork,
|
|
30
|
+
{ networkType: ConfigNetworkType.EVM }
|
|
31
|
+
>;
|
|
32
|
+
|
|
33
|
+
export type PrimitiveEntryType = Extract<
|
|
34
|
+
PrimitiveEntry,
|
|
35
|
+
{ syncProtocol: ConfigSyncProtocolType.EVM_RPC_PARALLEL }
|
|
36
|
+
>;
|
|
37
|
+
|
|
38
|
+
export type PrimitiveType = FlattenSyncProtocolIOFor<
|
|
39
|
+
ConfigSyncProtocolType.EVM_RPC_PARALLEL
|
|
40
|
+
>;
|
|
41
|
+
export type Input = PageSyncRange<Page>;
|
|
42
|
+
export type Output = {
|
|
43
|
+
raw: GetBlockReturnType<Chain>;
|
|
44
|
+
primitives: PrimitiveType[];
|
|
45
|
+
blockHashes: EvmBlockHash[];
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* recall: EVM blocks use second resolution, but we want ms resolution
|
|
50
|
+
*/
|
|
51
|
+
export function toMsTimestamp(timestamp: bigint): TimestampMs {
|
|
52
|
+
return Number(timestamp) * 1000;
|
|
53
|
+
}
|