@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,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
|
+
}
|