@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,274 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ConfigSyncProtocolType,
|
|
3
|
+
type PrimitiveEntry,
|
|
4
|
+
} from "@effectstream/config";
|
|
5
|
+
import { BaseDataFetcher } from "../base/fetcher.ts";
|
|
6
|
+
import type { DataFetched } from "../base/fetcher.ts";
|
|
7
|
+
import type {
|
|
8
|
+
LastPage,
|
|
9
|
+
OutputAndCleanup,
|
|
10
|
+
RootConversion,
|
|
11
|
+
} from "../base/state.ts";
|
|
12
|
+
import type { RootOutput, RootPage } from "../types.ts";
|
|
13
|
+
import type {
|
|
14
|
+
ConfigType,
|
|
15
|
+
Input,
|
|
16
|
+
Output,
|
|
17
|
+
Page,
|
|
18
|
+
PrimitiveType,
|
|
19
|
+
} from "./types.ts";
|
|
20
|
+
import {
|
|
21
|
+
NearClient,
|
|
22
|
+
parseNep297Log,
|
|
23
|
+
type Nep297Event,
|
|
24
|
+
type NearExecutionOutcomeWithId,
|
|
25
|
+
} from "./NearClient.ts";
|
|
26
|
+
import { call, type Operation } from "effection";
|
|
27
|
+
import { bound } from "@effectstream/utils";
|
|
28
|
+
|
|
29
|
+
export class NearFetcher extends BaseDataFetcher<
|
|
30
|
+
Input,
|
|
31
|
+
Output,
|
|
32
|
+
RootOutput,
|
|
33
|
+
Page,
|
|
34
|
+
RootPage
|
|
35
|
+
> {
|
|
36
|
+
readonly client: NearClient;
|
|
37
|
+
|
|
38
|
+
constructor(
|
|
39
|
+
readonly config: ConfigType,
|
|
40
|
+
) {
|
|
41
|
+
super(config.syncProtocol.name);
|
|
42
|
+
this.client = new NearClient(config.network.rpcUrl);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@bound
|
|
46
|
+
override *readData(
|
|
47
|
+
data: Input,
|
|
48
|
+
rootConversion: RootConversion<Output, RootOutput, RootPage>,
|
|
49
|
+
lastPage: LastPage<Page, RootPage> | undefined,
|
|
50
|
+
): Operation<DataFetched<Output, Page, RootPage>> {
|
|
51
|
+
const outputs: OutputAndCleanup<Output>[] = [];
|
|
52
|
+
|
|
53
|
+
console.log(
|
|
54
|
+
`[NEAR] Fetching blocks from ${data.from} to ${data.to}.${
|
|
55
|
+
data.isPresync ? " [presync]" : ""
|
|
56
|
+
}`,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
for (let height = data.from; height <= data.to; height++) {
|
|
60
|
+
let block;
|
|
61
|
+
try {
|
|
62
|
+
block = yield* call(() =>
|
|
63
|
+
this.client.getBlock({ block_id: height })
|
|
64
|
+
);
|
|
65
|
+
} catch (_e) {
|
|
66
|
+
// Likely reached the chain tip; stop here
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const blockHash = block.header.hash;
|
|
71
|
+
// NEAR timestamps are in nanoseconds — convert to milliseconds
|
|
72
|
+
const timestampMs = Math.floor(
|
|
73
|
+
Number(block.header.timestamp_nanosec ?? block.header.timestamp) / 1_000_000,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const primitives = yield* this.readPrimitives(
|
|
77
|
+
height,
|
|
78
|
+
blockHash,
|
|
79
|
+
block.chunks,
|
|
80
|
+
this.config.primitives,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
outputs.push({
|
|
84
|
+
output: {
|
|
85
|
+
timestamp: timestampMs,
|
|
86
|
+
height,
|
|
87
|
+
hash: blockHash,
|
|
88
|
+
primitives,
|
|
89
|
+
},
|
|
90
|
+
cleanup: () => {},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (outputs.length === 0) {
|
|
95
|
+
if (!lastPage) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`[NEAR] Could not fetch any blocks from ${data.from} to ${data.to} and no previous page was found.`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
output: [],
|
|
102
|
+
lastPage,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const lastOutput = outputs[outputs.length - 1].output;
|
|
107
|
+
return {
|
|
108
|
+
output: outputs,
|
|
109
|
+
lastPage: {
|
|
110
|
+
ownBlockNumber: lastOutput.height,
|
|
111
|
+
own: {
|
|
112
|
+
height: lastOutput.height,
|
|
113
|
+
hash: lastOutput.hash,
|
|
114
|
+
},
|
|
115
|
+
root: rootConversion.toRootPage(lastOutput),
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@bound
|
|
121
|
+
*readPrimitives(
|
|
122
|
+
height: number,
|
|
123
|
+
blockHash: string,
|
|
124
|
+
chunkHeaders: { chunk_hash: string; shard_id: number }[],
|
|
125
|
+
primitiveEntries: Extract<
|
|
126
|
+
PrimitiveEntry,
|
|
127
|
+
{ syncProtocol: ConfigSyncProtocolType.NEAR_RPC_PARALLEL }
|
|
128
|
+
>[],
|
|
129
|
+
): Operation<PrimitiveType[]> {
|
|
130
|
+
if (primitiveEntries.length === 0) return [];
|
|
131
|
+
|
|
132
|
+
const allPrimitives: PrimitiveType[] = [];
|
|
133
|
+
|
|
134
|
+
// Fetch each chunk and extract events from transactions
|
|
135
|
+
for (const chunkHeader of chunkHeaders) {
|
|
136
|
+
let chunk;
|
|
137
|
+
try {
|
|
138
|
+
chunk = yield* call(() =>
|
|
139
|
+
this.client.getChunk(chunkHeader.chunk_hash)
|
|
140
|
+
);
|
|
141
|
+
} catch (_e) {
|
|
142
|
+
// Chunk may be empty or unavailable
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
for (const tx of chunk.transactions) {
|
|
147
|
+
let txStatus;
|
|
148
|
+
try {
|
|
149
|
+
txStatus = yield* call(() =>
|
|
150
|
+
this.client.getTxStatus(tx.hash, tx.signer_id)
|
|
151
|
+
);
|
|
152
|
+
} catch (_e) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Collect all execution outcomes (transaction outcome + receipt outcomes)
|
|
157
|
+
const allOutcomes: NearExecutionOutcomeWithId[] = [
|
|
158
|
+
txStatus.transaction_outcome,
|
|
159
|
+
...txStatus.receipts_outcome,
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
// Event-based primitives (Generic, Intent, NEP-141/171/245) match
|
|
163
|
+
// NEP-297 logs emitted during execution.
|
|
164
|
+
for (const outcomeWithId of allOutcomes) {
|
|
165
|
+
const { outcome } = outcomeWithId;
|
|
166
|
+
|
|
167
|
+
for (const log of outcome.logs) {
|
|
168
|
+
const event = parseNep297Log(log);
|
|
169
|
+
if (!event) continue;
|
|
170
|
+
|
|
171
|
+
for (const entry of primitiveEntries) {
|
|
172
|
+
const prim = entry.primitive;
|
|
173
|
+
// AccountWatch primitives have no event metadata; skip them here.
|
|
174
|
+
if (prim.eventStandard == null) continue;
|
|
175
|
+
if (!this.matchesPrimitive(outcome.executor_id, event, prim)) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
for (let i = 0; i < event.data.length; i++) {
|
|
180
|
+
allPrimitives.push({
|
|
181
|
+
syncProtocol: {
|
|
182
|
+
name: entry.syncProtocol,
|
|
183
|
+
blockNumber: height,
|
|
184
|
+
transactionHash: tx.hash,
|
|
185
|
+
contractAddress: outcome.executor_id,
|
|
186
|
+
logIndex: i,
|
|
187
|
+
},
|
|
188
|
+
primitive: prim.name,
|
|
189
|
+
output: {
|
|
190
|
+
payloadType: `${event.standard}:${event.event}`,
|
|
191
|
+
payload: {
|
|
192
|
+
...event.data[i] as Record<string, unknown>,
|
|
193
|
+
_standard: event.standard,
|
|
194
|
+
_version: event.version,
|
|
195
|
+
_event: event.event,
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// AccountWatch primitives capture raw execution-outcome data for any
|
|
205
|
+
// FunctionCall whose receiver matches the watched contractId, whether
|
|
206
|
+
// or not a NEP-297 event was logged. This is the non-event path.
|
|
207
|
+
const txStatusKind = txStatus.transaction_outcome.outcome.status;
|
|
208
|
+
const txSucceeded =
|
|
209
|
+
(txStatusKind as { SuccessValue?: string }).SuccessValue !== undefined ||
|
|
210
|
+
(txStatusKind as { SuccessReceiptId?: string }).SuccessReceiptId !== undefined;
|
|
211
|
+
const txStatusStr = txSucceeded ? "success" : "failure";
|
|
212
|
+
|
|
213
|
+
for (const entry of primitiveEntries) {
|
|
214
|
+
const prim = entry.primitive;
|
|
215
|
+
// Identify outcome-based primitives by the absence of event metadata.
|
|
216
|
+
if (prim.eventStandard != null) continue;
|
|
217
|
+
if (prim.contractId !== tx.receiver_id) continue;
|
|
218
|
+
|
|
219
|
+
for (let actionIndex = 0; actionIndex < tx.actions.length; actionIndex++) {
|
|
220
|
+
const action = tx.actions[actionIndex];
|
|
221
|
+
const fc = action.FunctionCall;
|
|
222
|
+
if (!fc) continue;
|
|
223
|
+
|
|
224
|
+
let argsDecoded = "";
|
|
225
|
+
try {
|
|
226
|
+
argsDecoded = Buffer.from(fc.args, "base64").toString("utf8");
|
|
227
|
+
} catch {
|
|
228
|
+
argsDecoded = fc.args;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
allPrimitives.push({
|
|
232
|
+
syncProtocol: {
|
|
233
|
+
name: entry.syncProtocol,
|
|
234
|
+
blockNumber: height,
|
|
235
|
+
transactionHash: tx.hash,
|
|
236
|
+
contractAddress: tx.receiver_id,
|
|
237
|
+
logIndex: actionIndex,
|
|
238
|
+
},
|
|
239
|
+
primitive: prim.name,
|
|
240
|
+
output: {
|
|
241
|
+
payloadType: "account-watch:function-call",
|
|
242
|
+
payload: {
|
|
243
|
+
signer_id: tx.signer_id,
|
|
244
|
+
receiver_id: tx.receiver_id,
|
|
245
|
+
method_name: fc.method_name,
|
|
246
|
+
args: argsDecoded,
|
|
247
|
+
deposit: fc.deposit,
|
|
248
|
+
status: txStatusStr,
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return allPrimitives;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private matchesPrimitive(
|
|
261
|
+
executorId: string,
|
|
262
|
+
event: Nep297Event,
|
|
263
|
+
prim: {
|
|
264
|
+
contractId: string;
|
|
265
|
+
eventStandard?: string;
|
|
266
|
+
eventType?: string;
|
|
267
|
+
},
|
|
268
|
+
): boolean {
|
|
269
|
+
if (prim.contractId !== executorId) return false;
|
|
270
|
+
if (prim.eventStandard && prim.eventStandard !== event.standard) return false;
|
|
271
|
+
if (prim.eventType && prim.eventType !== event.event) return false;
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { call, type Operation } from "effection";
|
|
2
|
+
import { bound } from "@effectstream/utils";
|
|
3
|
+
import type { PoolClient } from "pg";
|
|
4
|
+
import { type LastPage, SyncState } from "../base/state.ts";
|
|
5
|
+
import type { RootOutput, RootPage } from "../types.ts";
|
|
6
|
+
import type { Input, Output, Page } from "./types.ts";
|
|
7
|
+
import { pageRelation } from "./types.ts";
|
|
8
|
+
import type { NearFetcher } from "./fetcher.ts";
|
|
9
|
+
import type { ConfigNetworkType, SyncProtocolWithNetwork } from "@effectstream/config";
|
|
10
|
+
import { getPage } from "@effectstream/db";
|
|
11
|
+
import { NearClient } from "./NearClient.ts";
|
|
12
|
+
import { applyDelay } from "../common/utils.ts";
|
|
13
|
+
|
|
14
|
+
export class NearSyncState extends SyncState<
|
|
15
|
+
Input,
|
|
16
|
+
Output,
|
|
17
|
+
Page,
|
|
18
|
+
RootOutput,
|
|
19
|
+
RootPage,
|
|
20
|
+
NearFetcher
|
|
21
|
+
> {
|
|
22
|
+
constructor(
|
|
23
|
+
lastPage: LastPage<Page, RootPage> | undefined,
|
|
24
|
+
readonly config: Extract<
|
|
25
|
+
SyncProtocolWithNetwork,
|
|
26
|
+
{ networkType: ConfigNetworkType.NEAR }
|
|
27
|
+
>,
|
|
28
|
+
fetcher: NearFetcher,
|
|
29
|
+
public readonly client: NearClient,
|
|
30
|
+
dbConn: PoolClient,
|
|
31
|
+
) {
|
|
32
|
+
super(
|
|
33
|
+
config.syncProtocol.name,
|
|
34
|
+
lastPage,
|
|
35
|
+
fetcher,
|
|
36
|
+
pageRelation,
|
|
37
|
+
dbConn,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@bound
|
|
42
|
+
override toPage(_input: Input, data: Output[]): Page {
|
|
43
|
+
const lastBlock = data[data.length - 1];
|
|
44
|
+
return {
|
|
45
|
+
height: lastBlock.height,
|
|
46
|
+
hash: lastBlock.hash,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@bound
|
|
51
|
+
override toRootPage(data: Output): RootPage {
|
|
52
|
+
// NEAR timestamps are already in ms (converted in fetcher)
|
|
53
|
+
return applyDelay(data.timestamp, this.config.syncProtocol.delayMs);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@bound
|
|
57
|
+
override toRootOutput(_data: Output): RootOutput {
|
|
58
|
+
throw new Error("Only main chains create root outputs");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@bound
|
|
62
|
+
override *stateToInput(): Operation<Input | undefined> {
|
|
63
|
+
const latestHeight = yield* call(() =>
|
|
64
|
+
this.client.getLatestBlockHeight()
|
|
65
|
+
);
|
|
66
|
+
const startHeight = this.lastPage?.own.height ??
|
|
67
|
+
this.config.syncProtocol.startBlockHeight - 1;
|
|
68
|
+
|
|
69
|
+
if (startHeight >= latestHeight) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const from = startHeight + 1;
|
|
74
|
+
const to = Math.min(
|
|
75
|
+
from + this.config.syncProtocol.stepSize - 1,
|
|
76
|
+
latestHeight,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
from,
|
|
81
|
+
to,
|
|
82
|
+
isPresync: false,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@bound
|
|
87
|
+
override mergeDatum(ourOutput: Output, rootOutput: RootOutput): void {
|
|
88
|
+
const primitives = ourOutput.primitives.map((p) => ({
|
|
89
|
+
...p,
|
|
90
|
+
source: this.config.syncProtocol.name,
|
|
91
|
+
}));
|
|
92
|
+
const blockInfo = [{
|
|
93
|
+
protocol_name: this.config.syncProtocol.name,
|
|
94
|
+
block_number: ourOutput.height,
|
|
95
|
+
blockHash: ourOutput.hash,
|
|
96
|
+
}];
|
|
97
|
+
rootOutput.blockInfo.push(...blockInfo);
|
|
98
|
+
rootOutput.primitives.push(...primitives);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@bound
|
|
102
|
+
override getNamespace(): string[] {
|
|
103
|
+
return [this.config.network.name, this.config.syncProtocol.name];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
static *restoreState(
|
|
107
|
+
dbConn: PoolClient,
|
|
108
|
+
config: Extract<
|
|
109
|
+
SyncProtocolWithNetwork,
|
|
110
|
+
{ networkType: ConfigNetworkType.NEAR }
|
|
111
|
+
>,
|
|
112
|
+
fetcher: NearFetcher,
|
|
113
|
+
): Operation<NearSyncState> {
|
|
114
|
+
const [result] = yield* call(async () =>
|
|
115
|
+
await getPage.run({
|
|
116
|
+
protocol_name: config.syncProtocol.name,
|
|
117
|
+
}, dbConn)
|
|
118
|
+
);
|
|
119
|
+
const page = result
|
|
120
|
+
? result.page as unknown as LastPage<Page, RootPage>
|
|
121
|
+
: undefined;
|
|
122
|
+
return new NearSyncState(
|
|
123
|
+
page,
|
|
124
|
+
config,
|
|
125
|
+
fetcher,
|
|
126
|
+
new NearClient(config.network.rpcUrl),
|
|
127
|
+
dbConn,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ConfigSyncProtocolType,
|
|
3
|
+
FlattenSyncProtocolIOFor,
|
|
4
|
+
SyncProtocolWithNetwork,
|
|
5
|
+
} from "@effectstream/config";
|
|
6
|
+
import type { BlockNumber } from "@effectstream/utils";
|
|
7
|
+
import type { PageSyncRange } from "../common/page-helpers.ts";
|
|
8
|
+
import type { PageRelation } from "../base/page.ts";
|
|
9
|
+
|
|
10
|
+
// =====================
|
|
11
|
+
// Sync protocol typings
|
|
12
|
+
// =====================
|
|
13
|
+
|
|
14
|
+
/** The page identifier for NEAR: a block height + hash pair */
|
|
15
|
+
export type Page = {
|
|
16
|
+
height: BlockNumber;
|
|
17
|
+
hash: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type PrimitiveType = FlattenSyncProtocolIOFor<
|
|
21
|
+
ConfigSyncProtocolType.NEAR_RPC_PARALLEL
|
|
22
|
+
>;
|
|
23
|
+
|
|
24
|
+
export type Input = PageSyncRange<BlockNumber>;
|
|
25
|
+
|
|
26
|
+
export type Output = {
|
|
27
|
+
/** Block timestamp in milliseconds (converted from NEAR nanoseconds) */
|
|
28
|
+
timestamp: number;
|
|
29
|
+
/** Block height */
|
|
30
|
+
height: number;
|
|
31
|
+
/** Base58 block hash */
|
|
32
|
+
hash: string;
|
|
33
|
+
primitives: PrimitiveType[];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const pageRelation: PageRelation<Page> = {
|
|
37
|
+
compare: (p1, p2) => p1.height - p2.height,
|
|
38
|
+
equals: (p1, p2) => p1.height === p2.height,
|
|
39
|
+
min: (p1, p2) => (p1.height < p2.height ? p1 : p2),
|
|
40
|
+
max: (p1, p2) => (p1.height > p2.height ? p1 : p2),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type ConfigType = Extract<
|
|
44
|
+
SyncProtocolWithNetwork,
|
|
45
|
+
{ syncProtocolType: ConfigSyncProtocolType.NEAR_RPC_PARALLEL }
|
|
46
|
+
>;
|
|
@@ -0,0 +1,199 @@
|
|
|
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 { Operation } from "effection";
|
|
8
|
+
import { all, call, until } from "effection";
|
|
9
|
+
import { bound, type NtpBlockHash, type NtpPageJson } from "@effectstream/utils";
|
|
10
|
+
import { blockNumberRelation } from "../common/utils.ts";
|
|
11
|
+
import type { Input, Output, Page, PrimitiveType } from "./types.ts";
|
|
12
|
+
import {
|
|
13
|
+
fetchNewestPage,
|
|
14
|
+
genImmediatePageRequests,
|
|
15
|
+
genOnDemandPageRequests,
|
|
16
|
+
type PageRange,
|
|
17
|
+
type PageRequest,
|
|
18
|
+
} from "../base/page.ts";
|
|
19
|
+
import type { PrimitiveFetcher } from "../base/primitive.ts";
|
|
20
|
+
import type { RootConversion } from "../base/state.ts";
|
|
21
|
+
import type {
|
|
22
|
+
ConfigNetworkType,
|
|
23
|
+
PrimitiveEntry,
|
|
24
|
+
SyncProtocolWithNetwork,
|
|
25
|
+
} from "@effectstream/config";
|
|
26
|
+
import type { ConfigSyncProtocolType } from "@effectstream/config";
|
|
27
|
+
import { NtpTimeSync, type NtpTimeSyncDefaultOptions } from "ntp-time-sync";
|
|
28
|
+
|
|
29
|
+
type RecursivePartial<T> = {
|
|
30
|
+
[P in keyof T]?: RecursivePartial<T[P]>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export class NtpFetcher
|
|
34
|
+
extends BaseDataFetcher<Input, Output, RootOutput, Page, RootPage>
|
|
35
|
+
implements
|
|
36
|
+
PrimitiveFetcher<
|
|
37
|
+
Input,
|
|
38
|
+
Page,
|
|
39
|
+
{ blockNumber: number; timestamp: bigint; hash: string },
|
|
40
|
+
PrimitiveType,
|
|
41
|
+
ConfigSyncProtocolType.NTP_MAIN
|
|
42
|
+
>,
|
|
43
|
+
PaginatedFetcher<Page> {
|
|
44
|
+
readonly ntpTimeSync: NtpTimeSync;
|
|
45
|
+
constructor(
|
|
46
|
+
readonly config: Extract<
|
|
47
|
+
SyncProtocolWithNetwork,
|
|
48
|
+
{ networkType: ConfigNetworkType.NTP }
|
|
49
|
+
>,
|
|
50
|
+
) {
|
|
51
|
+
super(config.syncProtocol.name);
|
|
52
|
+
|
|
53
|
+
const options: RecursivePartial<typeof NtpTimeSyncDefaultOptions> = {
|
|
54
|
+
ntpDefaults: {
|
|
55
|
+
minPoll: 10,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
if (config.network.servers && config.network.servers.length) {
|
|
59
|
+
options.servers = config.network.servers;
|
|
60
|
+
}
|
|
61
|
+
// Cannot select options
|
|
62
|
+
// https://github.com/buffcode/ntp-time-sync/issues/104
|
|
63
|
+
this.ntpTimeSync = NtpTimeSync.getInstance();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@bound
|
|
67
|
+
override *readData(
|
|
68
|
+
data: Input,
|
|
69
|
+
rootConversion: RootConversion<Output, RootOutput, RootPage>,
|
|
70
|
+
): Operation<DataFetched<Output, Page, RootPage>> {
|
|
71
|
+
const pageFetcher = (() => {
|
|
72
|
+
if (data.isPresync) {
|
|
73
|
+
return genOnDemandPageRequests(
|
|
74
|
+
data.from,
|
|
75
|
+
data.to,
|
|
76
|
+
async (page) => {
|
|
77
|
+
const diff = BigInt(this.config.network.blockTimeMS * page);
|
|
78
|
+
const timestamp = BigInt(this.config.network.startTime) + diff;
|
|
79
|
+
return {
|
|
80
|
+
blockNumber: page,
|
|
81
|
+
timestamp,
|
|
82
|
+
hash: `0x${timestamp.toString(16)}`,
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
blockNumberRelation,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return genImmediatePageRequests(
|
|
89
|
+
Array.from(
|
|
90
|
+
{ length: data.to - data.from + 1 },
|
|
91
|
+
(_, i) => i + data.from,
|
|
92
|
+
),
|
|
93
|
+
async (page) => {
|
|
94
|
+
const diff = BigInt(this.config.network.blockTimeMS * page);
|
|
95
|
+
const timestamp = BigInt(this.config.network.startTime) + diff;
|
|
96
|
+
return {
|
|
97
|
+
blockNumber: page,
|
|
98
|
+
timestamp,
|
|
99
|
+
hash: `0x${timestamp.toString(16)}`,
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
})();
|
|
104
|
+
|
|
105
|
+
const output = yield* all(
|
|
106
|
+
Array.from(
|
|
107
|
+
{ length: data.to - data.from + 1 },
|
|
108
|
+
(_, i) => i + data.from,
|
|
109
|
+
).map(function* (page: Page) {
|
|
110
|
+
const raw = yield* call(() => pageFetcher(page));
|
|
111
|
+
const blockHashes = [raw.hash] as NtpBlockHash[];
|
|
112
|
+
return {
|
|
113
|
+
raw,
|
|
114
|
+
blockHashes,
|
|
115
|
+
};
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
return {
|
|
119
|
+
output: output.map((o) => ({
|
|
120
|
+
output: o,
|
|
121
|
+
cleanup: () => {}, // no cleanup required
|
|
122
|
+
})),
|
|
123
|
+
lastPage: {
|
|
124
|
+
ownBlockNumber: Number(data.to),
|
|
125
|
+
own: Number(data.to),
|
|
126
|
+
root: rootConversion.toRootPage({
|
|
127
|
+
blockHashes: [] as NtpBlockHash[],
|
|
128
|
+
raw: (yield* call(() => pageFetcher(Number(data.to)))),
|
|
129
|
+
}),
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@bound
|
|
135
|
+
groupByPage(
|
|
136
|
+
primitives: PrimitiveType[],
|
|
137
|
+
): Record<NtpPageJson, PrimitiveType[]> {
|
|
138
|
+
// Not used in NTP
|
|
139
|
+
return {};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@bound
|
|
143
|
+
*readPrimitives(
|
|
144
|
+
data: Input,
|
|
145
|
+
pageRequest: PageRequest<Page, { timestamp: bigint; hash: string }>,
|
|
146
|
+
primitiveEntries: Extract<
|
|
147
|
+
PrimitiveEntry,
|
|
148
|
+
{ syncProtocol: ConfigSyncProtocolType.NTP_MAIN }
|
|
149
|
+
>[],
|
|
150
|
+
): Operation<PrimitiveType[]> {
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@bound
|
|
155
|
+
*getLatestPage(
|
|
156
|
+
knownLastPage: undefined | Page,
|
|
157
|
+
): Operation<Page> {
|
|
158
|
+
const self = this;
|
|
159
|
+
return yield* fetchNewestPage<Page>(
|
|
160
|
+
blockNumberRelation,
|
|
161
|
+
function* (): Operation<Page> {
|
|
162
|
+
const result = yield* until(self.ntpTimeSync.getTime());
|
|
163
|
+
if (result.offset > 5000) {
|
|
164
|
+
console.error(
|
|
165
|
+
`NTP offset is higher than 5[s]. Please check your NTP server & system time configuration.
|
|
166
|
+
NTP time: ${result.now}
|
|
167
|
+
Local time: ${new Date()}
|
|
168
|
+
Offset [ms]: ${result.offset}`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
const diff = result.now.getTime() - self.config.network.startTime;
|
|
172
|
+
const blockNumber = Math.floor(diff / self.config.network.blockTimeMS);
|
|
173
|
+
return blockNumber;
|
|
174
|
+
},
|
|
175
|
+
knownLastPage,
|
|
176
|
+
this.config.syncProtocol.pollingInterval,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@bound
|
|
181
|
+
previousInterval(nextIntervalStart: Page): PageRange<Page> {
|
|
182
|
+
return this.intervalFromStart(
|
|
183
|
+
nextIntervalStart - this.config.syncProtocol.stepSize - 1,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
@bound
|
|
188
|
+
nextInterval(prevIntervalEnd: Page): PageRange<Page> {
|
|
189
|
+
return this.intervalFromStart(prevIntervalEnd + 1);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
@bound
|
|
193
|
+
intervalFromStart(start: Page): PageRange<Page> {
|
|
194
|
+
return {
|
|
195
|
+
from: start,
|
|
196
|
+
to: start + this.config.syncProtocol.stepSize,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|