@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.
Files changed (45) hide show
  1. package/package.json +29 -0
  2. package/src/mod.ts +3 -0
  3. package/src/sync-protocols/avail/AvailClient.ts +120 -0
  4. package/src/sync-protocols/avail/fetcher.ts +174 -0
  5. package/src/sync-protocols/avail/state.ts +134 -0
  6. package/src/sync-protocols/avail/types.ts +92 -0
  7. package/src/sync-protocols/base/fetcher.ts +43 -0
  8. package/src/sync-protocols/base/page.ts +89 -0
  9. package/src/sync-protocols/base/primitive.ts +24 -0
  10. package/src/sync-protocols/base/state.ts +143 -0
  11. package/src/sync-protocols/bitcoin/fetcher.ts +549 -0
  12. package/src/sync-protocols/bitcoin/state.ts +144 -0
  13. package/src/sync-protocols/bitcoin/types.ts +92 -0
  14. package/src/sync-protocols/celestia/CelestiaClient.ts +139 -0
  15. package/src/sync-protocols/celestia/fetcher.ts +165 -0
  16. package/src/sync-protocols/celestia/state.ts +131 -0
  17. package/src/sync-protocols/celestia/types.ts +46 -0
  18. package/src/sync-protocols/common/page-helpers.ts +65 -0
  19. package/src/sync-protocols/common/root.ts +35 -0
  20. package/src/sync-protocols/common/utils.ts +29 -0
  21. package/src/sync-protocols/evm/fetcher.ts +290 -0
  22. package/src/sync-protocols/evm/state.ts +122 -0
  23. package/src/sync-protocols/evm/types.ts +53 -0
  24. package/src/sync-protocols/midnight/MidnightClient.ts +210 -0
  25. package/src/sync-protocols/midnight/fetcher.ts +252 -0
  26. package/src/sync-protocols/midnight/state.ts +147 -0
  27. package/src/sync-protocols/midnight/types.ts +128 -0
  28. package/src/sync-protocols/midnight/zswap-decoder.ts +123 -0
  29. package/src/sync-protocols/mod.ts +29 -0
  30. package/src/sync-protocols/near/NearClient.ts +178 -0
  31. package/src/sync-protocols/near/fetcher.ts +274 -0
  32. package/src/sync-protocols/near/state.ts +130 -0
  33. package/src/sync-protocols/near/types.ts +46 -0
  34. package/src/sync-protocols/ntp/fetcher.ts +199 -0
  35. package/src/sync-protocols/ntp/state.ts +118 -0
  36. package/src/sync-protocols/ntp/types.ts +40 -0
  37. package/src/sync-protocols/orchestration/merge.ts +215 -0
  38. package/src/sync-protocols/orchestration/sync.ts +74 -0
  39. package/src/sync-protocols/types.ts +32 -0
  40. package/src/sync-protocols/utxorpc/BufferedRpc.ts +169 -0
  41. package/src/sync-protocols/utxorpc/fetcher.ts +116 -0
  42. package/src/sync-protocols/utxorpc/state.ts +186 -0
  43. package/src/sync-protocols/utxorpc/types.ts +53 -0
  44. package/src/sync-protocols/utxorpc/utils.ts +80 -0
  45. package/src/syncProtocolFactory.ts +149 -0
@@ -0,0 +1,210 @@
1
+ import { indexerPublicDataProvider } from "@midnight-ntwrk/midnight-js-indexer-public-data-provider";
2
+ import type { Block } from "./types.ts";
3
+ import type { ExecutionResult } from "graphql-ws/client";
4
+
5
+ type MidnightGqlBlock = {
6
+ block: Block;
7
+ };
8
+
9
+
10
+ export interface MidnightGqlBlockState {
11
+ block: {
12
+ hash: string;
13
+ height: number;
14
+ protocolVersion: number;
15
+ timestamp: number;
16
+ parent: {
17
+ hash: string;
18
+ };
19
+ transactions: {
20
+ hash: string;
21
+ contractActions: {
22
+ address: string;
23
+ state: string;
24
+ }[];
25
+ zswapLedgerEvents?: {
26
+ id: number;
27
+ raw: string;
28
+ maxId: number;
29
+ }[];
30
+ }[];
31
+ };
32
+ };
33
+
34
+ export interface BlockFetchOptions {
35
+ /** Include contractActions in the transaction fields (needed for MidnightGenericPrimitive). Default: true */
36
+ contractActions?: boolean;
37
+ /** Include zswapLedgerEvents in the transaction fields (needed for MidnightNullifierPrimitive). Default: true */
38
+ zswapLedgerEvents?: boolean;
39
+ }
40
+
41
+ type PublicDataProvider = ReturnType<typeof indexerPublicDataProvider>;
42
+ export class MidnightClient {
43
+ private readonly queryURL: string;
44
+ private readonly subscriptionURL: string;
45
+ private publicDataProvider: PublicDataProvider;
46
+ private readonly networkId?: string;
47
+ /** Timeout in milliseconds for individual HTTP requests to the indexer. */
48
+ private readonly requestTimeoutMs: number;
49
+
50
+ constructor(queryURL: string, subscriptionURL: string, networkId?: string, requestTimeoutMs = 30_000) {
51
+ this.queryURL = queryURL;
52
+ this.subscriptionURL = subscriptionURL;
53
+ this.networkId = networkId;
54
+ this.requestTimeoutMs = requestTimeoutMs;
55
+ this.publicDataProvider = indexerPublicDataProvider(
56
+ queryURL,
57
+ subscriptionURL,
58
+ );
59
+ console.log(
60
+ `[MidnightClient] Using indexer ${queryURL} (network: ${
61
+ networkId ?? "unknown"
62
+ })`,
63
+ );
64
+ }
65
+
66
+ /**
67
+ * Recreate the publicDataProvider to recover from stale internal state.
68
+ * The Midnight JS library can get into a state where all queryContractState
69
+ * calls fail with {"json":{}} until the provider is recreated.
70
+ */
71
+ resetProvider(): void {
72
+ console.log(
73
+ `[MidnightClient] Resetting publicDataProvider for ${this.queryURL}`,
74
+ );
75
+ this.publicDataProvider = indexerPublicDataProvider(
76
+ this.queryURL,
77
+ this.subscriptionURL,
78
+ );
79
+ }
80
+
81
+ async fetchContractState(contractAddress: string, blockHeight: number) {
82
+ try {
83
+ const state = await Promise.race([
84
+ this.publicDataProvider.queryContractState(
85
+ contractAddress,
86
+ {
87
+ type: "blockHeight",
88
+ blockHeight,
89
+ },
90
+ ),
91
+ new Promise<never>((_, reject) => {
92
+ setTimeout(
93
+ () => reject(new Error(`fetchContractState timed out after ${this.requestTimeoutMs}ms (contract=${contractAddress}, height=${blockHeight})`)),
94
+ this.requestTimeoutMs,
95
+ );
96
+ }),
97
+ ]);
98
+ return state;
99
+ } catch (error) {
100
+ // The Midnight JS publicDataProvider can enter a broken state where all
101
+ // subsequent calls fail (e.g. {"json":{}}). Recreate it so the next
102
+ // retry starts with a fresh connection.
103
+ this.resetProvider();
104
+ throw error;
105
+ }
106
+ }
107
+
108
+ async gqlQuery(query: string, signal?: AbortSignal): Promise<any> {
109
+ const timeoutSignal = AbortSignal.timeout(this.requestTimeoutMs);
110
+ const combinedSignal = signal
111
+ ? AbortSignal.any([signal, timeoutSignal])
112
+ : timeoutSignal;
113
+ const response = await fetch(this.queryURL, {
114
+ method: "POST",
115
+ body: JSON.stringify({ query }),
116
+ headers: {
117
+ "Content-Type": "application/json",
118
+ },
119
+ signal: combinedSignal,
120
+ });
121
+
122
+ if (!response.ok) {
123
+ console.error(`Failed to fetch ${this.queryURL}`);
124
+ console.error("Query:", JSON.stringify(query));
125
+ console.error("Response:", await response.text());
126
+ throw new GraphQLError(
127
+ `Server returned ${response.status} ${response.statusText}`,
128
+ );
129
+ }
130
+
131
+ const body = await response.json();
132
+ if ("errors" in body) {
133
+ throw new GraphQLError(
134
+ "Server returned errors",
135
+ body.errors as GraphQLErrorDetail[],
136
+ );
137
+ }
138
+ if ("data" in body) {
139
+ return body.data;
140
+ }
141
+ throw new GraphQLError("Server returned nothing");
142
+ }
143
+
144
+ handleGqlWsError<T>(
145
+ ex: IteratorResult<ExecutionResult<T, unknown>, unknown>,
146
+ ): T {
147
+ if (ex.done) throw new GraphQLError("Subscription ended");
148
+ if (ex.value.errors) {
149
+ throw new GraphQLError("Subscription errored", ex.value.errors);
150
+ }
151
+ if (!ex.value.data) throw new GraphQLError("Server returned nothing");
152
+ return ex.value.data;
153
+ }
154
+
155
+ async fetchBlock(
156
+ blockHeight: number,
157
+ options: BlockFetchOptions = {},
158
+ signal?: AbortSignal,
159
+ ): Promise<MidnightGqlBlockState> {
160
+ const { contractActions = true, zswapLedgerEvents = true } = options;
161
+ const contractActionsField = contractActions
162
+ ? `contractActions { address state }`
163
+ : "";
164
+ const zswapField = zswapLedgerEvents
165
+ ? `zswapLedgerEvents { id raw maxId }`
166
+ : "";
167
+ const query = `query {
168
+ block(offset: { height: ${blockHeight} }) {
169
+ hash
170
+ height
171
+ protocolVersion
172
+ timestamp
173
+ parent {
174
+ hash
175
+ }
176
+ transactions {
177
+ hash
178
+ ${contractActionsField}
179
+ ${zswapField}
180
+ }
181
+ }
182
+ }`;
183
+ return await this.gqlQuery(query, signal);
184
+ }
185
+
186
+ async fetchLatestBlock() {
187
+ const query = `query {
188
+ block {
189
+ height
190
+ }
191
+ }`;
192
+ return await this.gqlQuery(query);
193
+ }
194
+ }
195
+
196
+ interface GraphQLErrorDetail {
197
+ message: string;
198
+ locations?: readonly { line: number; column: number }[];
199
+ path?: readonly (string | number)[];
200
+ extensions?: object;
201
+ }
202
+
203
+ class GraphQLError extends Error {
204
+ errors?: readonly GraphQLErrorDetail[];
205
+
206
+ constructor(message: string, errors?: readonly GraphQLErrorDetail[]) {
207
+ super(message);
208
+ this.errors = errors;
209
+ }
210
+ }
@@ -0,0 +1,252 @@
1
+ import { BaseDataFetcher } from "../base/fetcher.ts";
2
+ import type {
3
+ Block,
4
+ ConfigType,
5
+ Input,
6
+ Output,
7
+ Page,
8
+ PrimitiveEntryType,
9
+ PrimitiveType,
10
+ } from "./types.ts";
11
+ import { all, call, type Operation, useAbortSignal } from "effection";
12
+ import type { DataFetched } from "../base/fetcher.ts";
13
+ import type {
14
+ OutputAndCleanup,
15
+ RootConversion,
16
+ } from "../base/state.ts";
17
+ import type { RootOutput, RootPage } from "../types.ts";
18
+ import { bound } from "@effectstream/utils";
19
+ import { MidnightClient, type BlockFetchOptions, type MidnightGqlBlockState } from "./MidnightClient.ts";
20
+ import { ContractState, StateValue } from "@midnight-ntwrk/onchain-runtime";
21
+ import { decodeZswapInputEvent } from "./zswap-decoder.ts";
22
+
23
+ export class MidnightFetcher extends BaseDataFetcher<
24
+ Input,
25
+ Output,
26
+ RootOutput,
27
+ Page,
28
+ RootPage
29
+ > {
30
+ readonly client: MidnightClient;
31
+ private readonly networkId?: string;
32
+ constructor(
33
+ readonly config: ConfigType,
34
+ ) {
35
+ super(config.syncProtocol.name);
36
+ const indexerHttp = config.syncProtocol.indexer;
37
+ const indexerWs =
38
+ (config.syncProtocol as any).indexerWS ??
39
+ (config.syncProtocol as any).indexerWs;
40
+ if (!indexerHttp || !indexerWs) {
41
+ throw new Error(
42
+ `Midnight sync protocol "${
43
+ config.syncProtocol.name
44
+ }" requires both indexer and indexerWS URLs. Received indexer=${
45
+ indexerHttp ?? "undefined"
46
+ }, indexerWS=${indexerWs ?? "undefined"}.`,
47
+ );
48
+ }
49
+ this.networkId = config.network?.networkId ??
50
+ (config.network as any)?.id;
51
+
52
+ if (this.networkId != null) {
53
+ for (const entry of config.primitives) {
54
+ const primitiveNetworkId = (entry.primitive as any).networkId;
55
+ if (primitiveNetworkId != null && primitiveNetworkId !== this.networkId) {
56
+ throw new Error(
57
+ `Midnight primitive "${entry.primitive.name}" has networkId "${primitiveNetworkId}" ` +
58
+ `but the network "${config.network.name}" uses networkId "${this.networkId}". ` +
59
+ `These must match.`,
60
+ );
61
+ }
62
+ }
63
+ }
64
+
65
+ this.client = new MidnightClient(
66
+ indexerHttp,
67
+ indexerWs,
68
+ this.networkId,
69
+ );
70
+ }
71
+
72
+ @bound
73
+ override *readData(
74
+ data: Input,
75
+ rootConversion: RootConversion<Output, RootOutput, RootPage>,
76
+ ): Operation<DataFetched<Output, Page, RootPage>> {
77
+ const outputs: OutputAndCleanup<Output>[] = [];
78
+ console.log(
79
+ `[Midnight${this.networkId ? `:${this.networkId}` : ""}] Fetching blocks from ${data.from} to ${data.to}. ${
80
+ data.isPresync ? "[presync]" : ""
81
+ }`,
82
+ );
83
+ const blockFetchOptions: BlockFetchOptions = {
84
+ contractActions: this.config.primitives.some(
85
+ (p) => p.primitive.type !== "Midnight:Nullifier",
86
+ ),
87
+ zswapLedgerEvents: this.config.primitives.some(
88
+ (p) => p.primitive.type === "Midnight:Nullifier",
89
+ ),
90
+ };
91
+ const self = this;
92
+ const heights = Array.from(
93
+ { length: data.to - data.from + 1 },
94
+ (_, i) => i + data.from,
95
+ );
96
+ const fetched = yield* all(
97
+ // The endpoint does not support range requests,
98
+ // so we fetch each block individually in parallel.
99
+ // useAbortSignal() ties each fetch's lifetime to its Effection scope,
100
+ // so cancellation by all() cleanly aborts the underlying HTTP request.
101
+ heights.map(function* (height) {
102
+ const signal = yield* useAbortSignal();
103
+ const result: MidnightGqlBlockState = yield* call(() =>
104
+ self.client.fetchBlock(height, blockFetchOptions, signal)
105
+ );
106
+ const primitives = yield* self.readPrimitives(
107
+ height,
108
+ result,
109
+ self.config.primitives,
110
+ );
111
+ return {
112
+ output: {
113
+ raw: result.block as unknown as Block,
114
+ primitives,
115
+ },
116
+ cleanup: () => {},
117
+ };
118
+ }),
119
+ );
120
+ outputs.push(...fetched);
121
+
122
+ const lastOutput = outputs[outputs.length - 1].output;
123
+ return {
124
+ output: outputs,
125
+ lastPage: {
126
+ ownBlockNumber: lastOutput.raw.height,
127
+ own: {
128
+ height: lastOutput.raw.height,
129
+ hash: lastOutput.raw.hash,
130
+ },
131
+ root: rootConversion.toRootPage(lastOutput),
132
+ },
133
+ };
134
+ }
135
+
136
+ @bound
137
+ *readPrimitives(
138
+ height: number,
139
+ block: MidnightGqlBlockState,
140
+ primitiveEntries: PrimitiveEntryType[],
141
+ ): Operation<PrimitiveType[]> {
142
+ const client = this.client;
143
+ const asyncOps: Operation<PrimitiveType[]>[] = [];
144
+ const syncResults: PrimitiveType[] = [];
145
+
146
+ for (const primitiveEntry of primitiveEntries) {
147
+ if (primitiveEntry.primitive.type === "Midnight:Nullifier") {
148
+ syncResults.push(...this.fetchNullifiers(height, primitiveEntry, block));
149
+ } else {
150
+ asyncOps.push(
151
+ this.fetchContractState(height, client, primitiveEntry, block),
152
+ );
153
+ }
154
+ }
155
+
156
+ const asyncResults = (yield* all(asyncOps)).flat();
157
+ return [...syncResults, ...asyncResults].filter(Boolean) as PrimitiveType[];
158
+ }
159
+
160
+ @bound
161
+ fetchNullifiers(
162
+ height: number,
163
+ primitiveEntry: PrimitiveEntryType,
164
+ block: MidnightGqlBlockState,
165
+ ): PrimitiveType[] {
166
+ const results: PrimitiveType[] = [];
167
+ for (const tx of block.block.transactions) {
168
+ for (const event of tx.zswapLedgerEvents ?? []) {
169
+ const decoded = decodeZswapInputEvent(event.raw);
170
+ if (!decoded) continue;
171
+ results.push({
172
+ syncProtocol: {
173
+ name: primitiveEntry.syncProtocol,
174
+ blockNumber: height,
175
+ transactionHash: tx.hash,
176
+ contractAddress: "",
177
+ },
178
+ primitive: primitiveEntry.primitive.name,
179
+ output: {
180
+ payloadType: "midnight-nullifier",
181
+ payload: {
182
+ nullifier: decoded.nullifier,
183
+ txHash: decoded.txHash,
184
+ eventId: event.id,
185
+ logicalSegment: decoded.logicalSegment,
186
+ },
187
+ },
188
+ });
189
+ }
190
+ }
191
+ return results;
192
+ }
193
+
194
+ @bound
195
+ *fetchContractState(
196
+ height: number,
197
+ client: MidnightClient,
198
+ primitiveEntry: PrimitiveEntryType,
199
+ block: MidnightGqlBlockState,
200
+ ): Operation<PrimitiveType[]> {
201
+ const contractAddress = primitiveEntry.primitive.contractAddress!;
202
+ const blockFinalState = yield* call(() =>
203
+ client.fetchContractState(contractAddress, height)
204
+ );
205
+ // The state returns null if the contract was not called in the block height
206
+ if (!blockFinalState) {
207
+ return [];
208
+ }
209
+
210
+ const transactions = block.block.transactions.filter((t) => {
211
+ return (t.contractActions ?? []).some((c) => {
212
+ const longest = Math.max(contractAddress.length, c.address.length);
213
+ // Addresses length can be padded by 0's
214
+ return c.address.padStart(longest, '0') === contractAddress.padStart(longest, '0');
215
+ });
216
+ });
217
+
218
+
219
+ return transactions.map(t => {
220
+ // TODO: What does it mean if t.contractActions has more than one element?
221
+ const rawState: string = t.contractActions.find((c) => {
222
+ const longest = Math.max(contractAddress.length, c.address.length);
223
+ // Addresses length can be padded by 0's
224
+ return c.address.padStart(longest, '0') === contractAddress.padStart(longest, '0');
225
+ })!.state!;
226
+ const byteState = new Uint8Array(rawState.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)));
227
+ const primitive = primitiveEntry.primitive;
228
+ const contractState = ContractState.deserialize(byteState);
229
+ const stateValue = contractState.data.state;
230
+ // Parse additional fields from the schema (defined per-contract on the primitive)
231
+ const additionalFields = primitive.parseAdditionalLedgerFields?.(stateValue) ?? {};
232
+ // Keep generated ledger fields, but let schema-parsed fields override.
233
+ // This is important for map-like fields where contract.ledger() exposes
234
+ // iterable wrappers that are not directly JSON-serializable.
235
+ const f = primitive.contract ?? { ledger: (_: StateValue): Record<string, any> => ({}) };
236
+ const state = { ...f.ledger(stateValue), ...additionalFields };
237
+ return {
238
+ syncProtocol: {
239
+ name: primitiveEntry.syncProtocol,
240
+ blockNumber: height,
241
+ transactionHash: t.hash,
242
+ contractAddress: contractAddress,
243
+ },
244
+ primitive: primitiveEntry.primitive.name,
245
+ output: {
246
+ payloadType: "midnight-contract-state",
247
+ payload: state,
248
+ },
249
+ }
250
+ });
251
+ }
252
+ }
@@ -0,0 +1,147 @@
1
+ import { call, type Operation } from "effection";
2
+ import { bound, type TimestampMs } 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 { MidnightFetcher } from "./fetcher.ts";
9
+ import type { ConfigNetworkType, SyncProtocolWithNetwork } from "@effectstream/config";
10
+ import { getPage } from "@effectstream/db";
11
+ import { MidnightClient } from "./MidnightClient.ts";
12
+ import { applyDelay } from "../common/utils.ts";
13
+
14
+ type LatestBlock = {
15
+ block: {
16
+ height: number;
17
+ };
18
+ };
19
+
20
+ export class MidnightSyncState extends SyncState<
21
+ Input,
22
+ Output,
23
+ Page,
24
+ RootOutput,
25
+ RootPage,
26
+ MidnightFetcher
27
+ > {
28
+ private readonly url: string;
29
+
30
+ constructor(
31
+ lastPage: LastPage<Page, RootPage> | undefined,
32
+ readonly config: Extract<
33
+ SyncProtocolWithNetwork,
34
+ { networkType: ConfigNetworkType.MIDNIGHT }
35
+ >,
36
+ fetcher: MidnightFetcher,
37
+ public readonly client: MidnightClient,
38
+ dbConn: PoolClient,
39
+ ) {
40
+ super(
41
+ config.syncProtocol.name,
42
+ lastPage,
43
+ fetcher,
44
+ pageRelation,
45
+ dbConn,
46
+ );
47
+ this.url = config.syncProtocol.indexer;
48
+ }
49
+
50
+ @bound
51
+ override toPage(input: Input, data: Output[]): Page {
52
+ const lastBlock = data[data.length - 1];
53
+ return {
54
+ height: lastBlock.raw.height,
55
+ hash: lastBlock.raw.hash,
56
+ };
57
+ }
58
+
59
+ @bound
60
+ override toRootPage(data: Output): RootPage {
61
+ const time = data.raw.timestamp as unknown as TimestampMs;
62
+ return applyDelay(
63
+ time,
64
+ this.config.syncProtocol.delayMs,
65
+ );
66
+ }
67
+
68
+ @bound
69
+ override toRootOutput(data: Output): RootOutput {
70
+ throw new Error("Only main chains create root outputs");
71
+ }
72
+
73
+ @bound
74
+ override *stateToInput(): Operation<Input | undefined> {
75
+ const latestBlockResult = yield* call(() => this.client.fetchLatestBlock());
76
+ const latestHeight = latestBlockResult.block.height;
77
+
78
+ const startHeight = this.lastPage?.own.height ??
79
+ this.config.syncProtocol.startBlockHeight - 1;
80
+
81
+ if (startHeight >= latestHeight) {
82
+ return undefined;
83
+ }
84
+
85
+ const from = startHeight + 1;
86
+ const to = Math.min(
87
+ from + this.config.syncProtocol.stepSize - 1,
88
+ latestHeight,
89
+ );
90
+
91
+ return {
92
+ from,
93
+ to,
94
+ isPresync: false, // TODO: handle presync
95
+ };
96
+ }
97
+
98
+ @bound
99
+ override mergeDatum(ourOutput: Output, rootOutput: RootOutput): void {
100
+ const primitives = ourOutput.primitives.map((p) => ({
101
+ ...p,
102
+ source: this.config.syncProtocol.name,
103
+ }));
104
+ const blockInfo = [{
105
+ protocol_name: this.config.syncProtocol.name,
106
+ block_number: ourOutput.raw.height,
107
+ blockHash: ourOutput.raw.hash,
108
+ }];
109
+ rootOutput.blockInfo.push(...blockInfo);
110
+ rootOutput.primitives.push(...primitives);
111
+ }
112
+
113
+ @bound
114
+ override getNamespace(): string[] {
115
+ return [this.config.network.name, this.config.syncProtocol.name];
116
+ }
117
+
118
+ static *restoreState(
119
+ dbConn: PoolClient,
120
+ config: Extract<
121
+ SyncProtocolWithNetwork,
122
+ { networkType: ConfigNetworkType.MIDNIGHT }
123
+ >,
124
+ fetcher: MidnightFetcher,
125
+ ): Operation<MidnightSyncState> {
126
+ const [result] = yield* call(async () =>
127
+ await getPage.run({
128
+ protocol_name: config.syncProtocol.name,
129
+ }, dbConn)
130
+ );
131
+ const page = result
132
+ ? result.page as unknown as LastPage<Page, RootPage>
133
+ : undefined;
134
+ return new MidnightSyncState(
135
+ page,
136
+ config,
137
+ fetcher,
138
+ // TODO: The urls should be part of the config.
139
+ new MidnightClient(
140
+ config.syncProtocol.indexer,
141
+ config.syncProtocol.indexerWS ??
142
+ "ws://127.0.0.1:8088/api/v3/graphql/ws",
143
+ ),
144
+ dbConn,
145
+ );
146
+ }
147
+ }