@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
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@effectstream/sync",
|
|
3
|
+
"version": "0.100.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"exports": "./src/mod.ts",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@effectstream/config": "0.100.0",
|
|
8
|
+
"@effectstream/db": "0.100.0",
|
|
9
|
+
"@effectstream/log": "0.100.0",
|
|
10
|
+
"@effectstream/utils": "0.100.0",
|
|
11
|
+
"@midnight-ntwrk/ledger-v8": "8.0.3",
|
|
12
|
+
"@midnight-ntwrk/midnight-js-indexer-public-data-provider": "4.0.2",
|
|
13
|
+
"@midnight-ntwrk/onchain-runtime": "npm:@midnight-ntwrk/onchain-runtime-v3@3.0.0",
|
|
14
|
+
"effection": "3.5.0",
|
|
15
|
+
"graphql-ws": "^6.0.6",
|
|
16
|
+
"ntp-time-sync": "^0.5.0",
|
|
17
|
+
"viem": "2.37.3",
|
|
18
|
+
"denque": "^2.1.0",
|
|
19
|
+
"pg": "^8.14.0",
|
|
20
|
+
"json-stable-stringify": "^1.2.1",
|
|
21
|
+
"@cardano-sdk/core": "^0.46.11",
|
|
22
|
+
"@cardano-sdk/util": "^0.17.1",
|
|
23
|
+
"@utxorpc/sdk": "^0.8.0",
|
|
24
|
+
"@utxorpc/spec": "^0.18.1",
|
|
25
|
+
"@sinclair/typebox": "0.34.41",
|
|
26
|
+
"avail-js-sdk": "^0.4.2",
|
|
27
|
+
"buffer": "^6.0.3"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/mod.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { SDK } from "avail-js-sdk";
|
|
2
|
+
import type {
|
|
3
|
+
AvailBlock,
|
|
4
|
+
AvailBlockDataItem,
|
|
5
|
+
AvailBlockHeader,
|
|
6
|
+
AvailStatus,
|
|
7
|
+
} from "./types.ts";
|
|
8
|
+
|
|
9
|
+
const AVAIL_CLIENT_DEFAULT_URL = "http://localhost:7007";
|
|
10
|
+
const AVAIL_NODE_DEFAULT_URL = "ws://localhost:9955/ws";
|
|
11
|
+
|
|
12
|
+
export class AvailClient {
|
|
13
|
+
private readonly url: string;
|
|
14
|
+
private readonly sdk: Promise<SDK>;
|
|
15
|
+
constructor(nodeUrl: string, lightClientUrl: string) {
|
|
16
|
+
this.url = lightClientUrl ?? AVAIL_CLIENT_DEFAULT_URL;
|
|
17
|
+
this.sdk = SDK.New(nodeUrl ?? AVAIL_NODE_DEFAULT_URL);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async getStatus(): Promise<AvailStatus> {
|
|
21
|
+
const response = await fetch(`${this.url}/v2/status`);
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`Failed to get status from light client, status: ${response.status}`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
const status: AvailStatus = await response.json();
|
|
28
|
+
if (!status) {
|
|
29
|
+
throw new Error(`Failed to get status from light client, no status found`);
|
|
30
|
+
}
|
|
31
|
+
return status;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async getLatestBlockHeight(): Promise<number> {
|
|
35
|
+
const api = (await this.sdk).client.api;
|
|
36
|
+
// Block from avail node is slightly above the light client
|
|
37
|
+
// const header = await api.rpc.chain.getHeader();
|
|
38
|
+
const status: AvailStatus = await this.getStatus();
|
|
39
|
+
|
|
40
|
+
// If the available field is present, this is the real latest block
|
|
41
|
+
// synced by the light client for the app-id
|
|
42
|
+
// otherwise the app id is yet not available.
|
|
43
|
+
if (status.blocks.available) {
|
|
44
|
+
return status.blocks.available.last;
|
|
45
|
+
} else {
|
|
46
|
+
return status.blocks.latest - 1;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async getBlockFromHash(hash: string): Promise<AvailBlock> {
|
|
51
|
+
const block = await (await this.sdk).client.api.rpc.chain.getBlock(hash);
|
|
52
|
+
return {
|
|
53
|
+
header: block.block.header as unknown as AvailBlockHeader,
|
|
54
|
+
extrinsics: block.block.extrinsics as unknown as string[],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async getBlockHeaderFromHeight(height: number): Promise<AvailBlockHeader> {
|
|
59
|
+
const status = await this.getStatus();
|
|
60
|
+
if (!status.blocks.available || height < status.blocks.available.first) {
|
|
61
|
+
// This is before the app is deployed; return a dummy header
|
|
62
|
+
// At this point in time, the app is still not deployed
|
|
63
|
+
return {
|
|
64
|
+
hash: "0x" + String(height) as `0x${string}`,
|
|
65
|
+
parent_hash: "0x0",
|
|
66
|
+
number: height,
|
|
67
|
+
state_root: "0x0",
|
|
68
|
+
extrinsics_root: "0x0",
|
|
69
|
+
extension: {
|
|
70
|
+
rows: 0,
|
|
71
|
+
cols: 0,
|
|
72
|
+
data_root: "0x0",
|
|
73
|
+
commitments: [],
|
|
74
|
+
app_lookup: {
|
|
75
|
+
size: 0,
|
|
76
|
+
index: [],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
received_at: 0,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const response = await fetch(`${this.url}/v2/blocks/${height}/header`);
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Failed to get block header from height ${height}, status: ${response.status}`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
const blockHeader = await response.json() as AvailBlockHeader;
|
|
90
|
+
if (!blockHeader) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Failed to get block header from height ${height}, no header found`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return blockHeader;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async getBlockDataFromHeight(height: number): Promise<AvailBlockDataItem> {
|
|
99
|
+
const status = await this.getStatus();
|
|
100
|
+
if (!status.blocks.available || height < status.blocks.available.first) {
|
|
101
|
+
// The app has no data or has not been deployed yet
|
|
102
|
+
// So we return a dummy empty data item
|
|
103
|
+
return { block_number: height, data_transactions: [] }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const response = await fetch(`${this.url}/v2/blocks/${height}/data`);
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Failed to get block data from height ${height}, status: ${response.status}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
const blockData = await response.json() as AvailBlockDataItem;
|
|
113
|
+
if (!blockData) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Failed to get block data from height ${height}, no data found`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return blockData;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ConfigSyncProtocolType,
|
|
3
|
+
type PrimitiveEntry,
|
|
4
|
+
type SyncProtocolWithNetwork,
|
|
5
|
+
} from "@effectstream/config";
|
|
6
|
+
import { BaseDataFetcher } from "../base/fetcher.ts";
|
|
7
|
+
import type { DataFetched } from "../base/fetcher.ts";
|
|
8
|
+
import type {
|
|
9
|
+
LastPage,
|
|
10
|
+
OutputAndCleanup,
|
|
11
|
+
RootConversion,
|
|
12
|
+
} from "../base/state.ts";
|
|
13
|
+
import type { RootOutput, RootPage } from "../types.ts";
|
|
14
|
+
import type {
|
|
15
|
+
ConfigType,
|
|
16
|
+
Input,
|
|
17
|
+
Output,
|
|
18
|
+
Page,
|
|
19
|
+
PrimitiveType,
|
|
20
|
+
} from "./types.ts";
|
|
21
|
+
import { AvailClient } from "./AvailClient.ts";
|
|
22
|
+
import { all, call, type Operation, sleep } from "effection";
|
|
23
|
+
import { bound } from "@effectstream/utils";
|
|
24
|
+
|
|
25
|
+
export class AvailFetcher extends BaseDataFetcher<
|
|
26
|
+
Input,
|
|
27
|
+
Output,
|
|
28
|
+
RootOutput,
|
|
29
|
+
Page,
|
|
30
|
+
RootPage
|
|
31
|
+
> {
|
|
32
|
+
readonly client: AvailClient;
|
|
33
|
+
|
|
34
|
+
constructor(
|
|
35
|
+
readonly config: ConfigType,
|
|
36
|
+
) {
|
|
37
|
+
super(config.syncProtocol.name);
|
|
38
|
+
this.client = new AvailClient(
|
|
39
|
+
config.syncProtocol.rpc,
|
|
40
|
+
config.syncProtocol.lightClient,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@bound
|
|
45
|
+
override *readData(
|
|
46
|
+
data: Input,
|
|
47
|
+
rootConversion: RootConversion<Output, RootOutput, RootPage>,
|
|
48
|
+
lastPage: LastPage<Page, RootPage> | undefined,
|
|
49
|
+
): Operation<DataFetched<Output, Page, RootPage>> {
|
|
50
|
+
const outputs: OutputAndCleanup<Output>[] = [];
|
|
51
|
+
console.log(
|
|
52
|
+
`[Avail] Fetching blocks from ${data.from} to ${data.to}. ${
|
|
53
|
+
data.isPresync ? "[presync]" : ""
|
|
54
|
+
}`,
|
|
55
|
+
);
|
|
56
|
+
for (let height = data.from; height <= data.to; height++) {
|
|
57
|
+
let header;
|
|
58
|
+
try {
|
|
59
|
+
header = yield* call(() =>
|
|
60
|
+
this.client.getBlockHeaderFromHeight(height)
|
|
61
|
+
);
|
|
62
|
+
} catch (_e) {
|
|
63
|
+
// Likely reached the tip; stop here
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const primitives = yield* this.readPrimitives(
|
|
68
|
+
height,
|
|
69
|
+
header,
|
|
70
|
+
this.config.primitives,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
outputs.push({
|
|
74
|
+
output: {
|
|
75
|
+
raw: header,
|
|
76
|
+
primitives,
|
|
77
|
+
},
|
|
78
|
+
cleanup: () => {},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (outputs.length === 0) {
|
|
83
|
+
if (!lastPage) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`Could not fetch any blocks from ${data.from} to ${data.to} and no previous page was found.`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
output: [],
|
|
90
|
+
lastPage,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const lastOutput = outputs[outputs.length - 1].output;
|
|
95
|
+
return {
|
|
96
|
+
output: outputs,
|
|
97
|
+
lastPage: {
|
|
98
|
+
ownBlockNumber: lastOutput.raw.number,
|
|
99
|
+
own: {
|
|
100
|
+
height: lastOutput.raw.number,
|
|
101
|
+
hash: lastOutput.raw.hash,
|
|
102
|
+
},
|
|
103
|
+
root: rootConversion.toRootPage(lastOutput),
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@bound
|
|
109
|
+
*readPrimitives(
|
|
110
|
+
height: number,
|
|
111
|
+
header: Output["raw"],
|
|
112
|
+
primitiveEntries: Extract<
|
|
113
|
+
PrimitiveEntry,
|
|
114
|
+
{ syncProtocol: ConfigSyncProtocolType.AVAIL_PARALLEL }
|
|
115
|
+
>[],
|
|
116
|
+
): Operation<PrimitiveType[]> {
|
|
117
|
+
const allOperations: Operation<PrimitiveType | undefined>[] = [];
|
|
118
|
+
for (const primitiveEntry of primitiveEntries) {
|
|
119
|
+
allOperations.push(
|
|
120
|
+
this.fetchData(height, primitiveEntry, header),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
return (yield* all(allOperations)).flat().filter(
|
|
124
|
+
Boolean,
|
|
125
|
+
) as PrimitiveType[];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@bound
|
|
129
|
+
*fetchData(
|
|
130
|
+
height: number,
|
|
131
|
+
primitiveEntry: Extract<
|
|
132
|
+
PrimitiveEntry,
|
|
133
|
+
{ syncProtocol: ConfigSyncProtocolType.AVAIL_PARALLEL }
|
|
134
|
+
>,
|
|
135
|
+
header: Output["raw"],
|
|
136
|
+
): Operation<PrimitiveType | undefined> {
|
|
137
|
+
// For now we only read blocks; if the appId appears in the block header
|
|
138
|
+
// we will invoke the data endpoint, but we will not emit a primitive yet.
|
|
139
|
+
const appId = primitiveEntry.primitive.appId;
|
|
140
|
+
if (typeof appId !== "number") return undefined;
|
|
141
|
+
|
|
142
|
+
const isPresent = header.extension.app_lookup.index.some((e) =>
|
|
143
|
+
e.appId === appId
|
|
144
|
+
);
|
|
145
|
+
if (!isPresent) return undefined;
|
|
146
|
+
// Read block data for the corresponding height to unblock future parsing.
|
|
147
|
+
// Intentionally ignore the result for now.
|
|
148
|
+
const blockData = yield* call(() =>
|
|
149
|
+
this.client.getBlockDataFromHeight(height)
|
|
150
|
+
);
|
|
151
|
+
// TODO: handle multiple data transactions
|
|
152
|
+
const dataTransaction = blockData.data_transactions.at(0);
|
|
153
|
+
if (!dataTransaction) return undefined;
|
|
154
|
+
// TODO: is always a JSON string?
|
|
155
|
+
const data: string = atob(dataTransaction.data);
|
|
156
|
+
return {
|
|
157
|
+
syncProtocol: {
|
|
158
|
+
name: primitiveEntry.syncProtocol,
|
|
159
|
+
blockNumber: height,
|
|
160
|
+
transactionHash: header.hash,
|
|
161
|
+
contractAddress: primitiveEntry.primitive.applicationKey,
|
|
162
|
+
},
|
|
163
|
+
primitive: primitiveEntry.primitive.name,
|
|
164
|
+
output: {
|
|
165
|
+
payloadType: "avail-app-state",
|
|
166
|
+
payload: {
|
|
167
|
+
inputData: height.toString(),
|
|
168
|
+
inputNonce: "0x", // TODO replace this with something meaningful
|
|
169
|
+
suppliedValue: data,
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
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 { AvailFetcher } from "./fetcher.ts";
|
|
9
|
+
import type { ConfigNetworkType, SyncProtocolWithNetwork } from "@effectstream/config";
|
|
10
|
+
import { getPage } from "@effectstream/db";
|
|
11
|
+
import { AvailClient } from "./AvailClient.ts";
|
|
12
|
+
import { applyDelay } from "../common/utils.ts";
|
|
13
|
+
|
|
14
|
+
export class AvailSyncState extends SyncState<
|
|
15
|
+
Input,
|
|
16
|
+
Output,
|
|
17
|
+
Page,
|
|
18
|
+
RootOutput,
|
|
19
|
+
RootPage,
|
|
20
|
+
AvailFetcher
|
|
21
|
+
> {
|
|
22
|
+
constructor(
|
|
23
|
+
lastPage: LastPage<Page, RootPage> | undefined,
|
|
24
|
+
readonly config: Extract<
|
|
25
|
+
SyncProtocolWithNetwork,
|
|
26
|
+
{ networkType: ConfigNetworkType.AVAIL }
|
|
27
|
+
>,
|
|
28
|
+
fetcher: AvailFetcher,
|
|
29
|
+
public readonly client: AvailClient,
|
|
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.raw.number,
|
|
46
|
+
hash: lastBlock.raw.hash,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@bound
|
|
51
|
+
override toRootPage(data: Output): RootPage {
|
|
52
|
+
// Avail headers include `received_at` (sec). Use it as the chain page.
|
|
53
|
+
return applyDelay(
|
|
54
|
+
data.raw.received_at * 1000,
|
|
55
|
+
this.config.syncProtocol.delayMs,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@bound
|
|
60
|
+
override toRootOutput(_data: Output): RootOutput {
|
|
61
|
+
throw new Error("Only main chains create root outputs");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@bound
|
|
65
|
+
override *stateToInput(): Operation<Input | undefined> {
|
|
66
|
+
const latestHeight = yield* call(() => this.client.getLatestBlockHeight());
|
|
67
|
+
const startHeight = this.lastPage?.own.height ??
|
|
68
|
+
this.config.syncProtocol.startBlockHeight - 1;
|
|
69
|
+
|
|
70
|
+
if (startHeight >= latestHeight) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const from = startHeight + 1;
|
|
75
|
+
const to = Math.min(
|
|
76
|
+
from + this.config.syncProtocol.stepSize - 1,
|
|
77
|
+
latestHeight,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
from,
|
|
82
|
+
to,
|
|
83
|
+
isPresync: false,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@bound
|
|
88
|
+
override mergeDatum(ourOutput: Output, rootOutput: RootOutput): void {
|
|
89
|
+
const primitives = ourOutput.primitives.map((p) => ({
|
|
90
|
+
...p,
|
|
91
|
+
source: this.config.syncProtocol.name,
|
|
92
|
+
}));
|
|
93
|
+
const blockInfo = [{
|
|
94
|
+
protocol_name: this.config.syncProtocol.name,
|
|
95
|
+
block_number: ourOutput.raw.number,
|
|
96
|
+
blockHash: ourOutput.raw.hash,
|
|
97
|
+
}];
|
|
98
|
+
rootOutput.blockInfo.push(...blockInfo);
|
|
99
|
+
rootOutput.primitives.push(...primitives);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@bound
|
|
103
|
+
override getNamespace(): string[] {
|
|
104
|
+
return [this.config.network.name, this.config.syncProtocol.name];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
static *restoreState(
|
|
108
|
+
dbConn: PoolClient,
|
|
109
|
+
config: Extract<
|
|
110
|
+
SyncProtocolWithNetwork,
|
|
111
|
+
{ networkType: ConfigNetworkType.AVAIL }
|
|
112
|
+
>,
|
|
113
|
+
fetcher: AvailFetcher,
|
|
114
|
+
): Operation<AvailSyncState> {
|
|
115
|
+
const [result] = yield* call(async () =>
|
|
116
|
+
await getPage.run({
|
|
117
|
+
protocol_name: config.syncProtocol.name,
|
|
118
|
+
}, dbConn)
|
|
119
|
+
);
|
|
120
|
+
const page = result
|
|
121
|
+
? result.page as unknown as LastPage<Page, RootPage>
|
|
122
|
+
: undefined;
|
|
123
|
+
return new AvailSyncState(
|
|
124
|
+
page,
|
|
125
|
+
config,
|
|
126
|
+
fetcher,
|
|
127
|
+
new AvailClient(
|
|
128
|
+
config.syncProtocol.rpc,
|
|
129
|
+
config.syncProtocol.lightClient,
|
|
130
|
+
),
|
|
131
|
+
dbConn,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
export type AvailStatus = {
|
|
11
|
+
modes: string[];
|
|
12
|
+
app_id: number;
|
|
13
|
+
genesis_hash: string;
|
|
14
|
+
network: string;
|
|
15
|
+
blocks: {
|
|
16
|
+
latest: number;
|
|
17
|
+
available?: {
|
|
18
|
+
first: number;
|
|
19
|
+
last: number;
|
|
20
|
+
};
|
|
21
|
+
app_data?: {
|
|
22
|
+
first: number;
|
|
23
|
+
last: number;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type AvailBlockHeader = {
|
|
29
|
+
hash: `0x${string}`;
|
|
30
|
+
parent_hash: `0x${string}`;
|
|
31
|
+
number: number;
|
|
32
|
+
state_root: `0x${string}`;
|
|
33
|
+
extrinsics_root: `0x${string}`;
|
|
34
|
+
extension: {
|
|
35
|
+
rows: number;
|
|
36
|
+
cols: number;
|
|
37
|
+
data_root: `0x${string}`;
|
|
38
|
+
commitments: string[];
|
|
39
|
+
app_lookup: {
|
|
40
|
+
size: number;
|
|
41
|
+
index: {
|
|
42
|
+
appId: number;
|
|
43
|
+
start: number;
|
|
44
|
+
}[];
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
received_at: number;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type AvailBlockDataItem = {
|
|
51
|
+
block_number: number;
|
|
52
|
+
data_transactions: {
|
|
53
|
+
data: string; // BASE64 encoded data
|
|
54
|
+
extrinsic: string;
|
|
55
|
+
}[];
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type AvailBlock = {
|
|
59
|
+
header: AvailBlockHeader;
|
|
60
|
+
extrinsics: string[];
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// =====================
|
|
64
|
+
// Sync protocol typings
|
|
65
|
+
// =====================
|
|
66
|
+
|
|
67
|
+
export type Page = {
|
|
68
|
+
height: BlockNumber;
|
|
69
|
+
hash: `0x${string}`;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type PrimitiveType = FlattenSyncProtocolIOFor<
|
|
73
|
+
ConfigSyncProtocolType.AVAIL_PARALLEL
|
|
74
|
+
>;
|
|
75
|
+
|
|
76
|
+
export type Input = PageSyncRange<BlockNumber>;
|
|
77
|
+
export type Output = {
|
|
78
|
+
raw: AvailBlockHeader;
|
|
79
|
+
primitives: PrimitiveType[];
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const pageRelation: PageRelation<Page> = {
|
|
83
|
+
compare: (p1, p2) => p1.height - p2.height,
|
|
84
|
+
equals: (p1, p2) => p1.height === p2.height,
|
|
85
|
+
min: (p1, p2) => (p1.height < p2.height ? p1 : p2),
|
|
86
|
+
max: (p1, p2) => (p1.height > p2.height ? p1 : p2),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export type ConfigType = Extract<
|
|
90
|
+
SyncProtocolWithNetwork,
|
|
91
|
+
{ syncProtocolType: ConfigSyncProtocolType.AVAIL_PARALLEL }
|
|
92
|
+
>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Channel, Operation } from "effection";
|
|
2
|
+
import { createChannel } from "effection";
|
|
3
|
+
import type { PageRange } from "./page.ts";
|
|
4
|
+
import type { LastPage, OutputAndCleanup, RootConversion } from "./state.ts";
|
|
5
|
+
|
|
6
|
+
export type DataFetched<Output, Page, RootPage> = {
|
|
7
|
+
output: OutputAndCleanup<Output>[];
|
|
8
|
+
lastPage: LastPage<Page, RootPage>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export abstract class BaseDataFetcher<
|
|
12
|
+
Input,
|
|
13
|
+
Output,
|
|
14
|
+
RootOutput,
|
|
15
|
+
Page,
|
|
16
|
+
RootPage,
|
|
17
|
+
> {
|
|
18
|
+
producerChannel: Channel<Output, void>;
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
readonly name: string,
|
|
22
|
+
) {
|
|
23
|
+
this.producerChannel = createChannel<Output>();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
abstract readData(
|
|
27
|
+
data: Input,
|
|
28
|
+
rootConversion: RootConversion<Output, RootOutput, RootPage>,
|
|
29
|
+
// TODO: Search for a better strategy so it's not needed. For now its just for mindnight
|
|
30
|
+
lastPage: LastPage<Page, RootPage> | undefined,
|
|
31
|
+
): Operation<DataFetched<Output, Page, RootPage>>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Useful interface when pagination is done in fixed predictable chunks
|
|
36
|
+
*/
|
|
37
|
+
export interface PaginatedFetcher<Page> {
|
|
38
|
+
getLatestPage(knownLastPage: undefined | Page): Operation<Page>;
|
|
39
|
+
|
|
40
|
+
previousInterval(nextIntervalStart: Page): PageRange<Page>;
|
|
41
|
+
nextInterval(prevIntervalEnd: Page): PageRange<Page>;
|
|
42
|
+
intervalFromStart(start: Page): PageRange<Page>;
|
|
43
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Operation } from "effection";
|
|
2
|
+
import { all, lift, sleep } from "effection";
|
|
3
|
+
import { retry } from "@effectstream/utils";
|
|
4
|
+
import type { IntervalMs, Json } from "@effectstream/utils";
|
|
5
|
+
import stableStringify from "json-stable-stringify";
|
|
6
|
+
|
|
7
|
+
export type PageRange<Page> = {
|
|
8
|
+
from: Page;
|
|
9
|
+
to: Page;
|
|
10
|
+
};
|
|
11
|
+
export interface PageRelation<T> {
|
|
12
|
+
/**
|
|
13
|
+
* -1 → p1 < p2 \
|
|
14
|
+
* 0 → p1 = p2 \
|
|
15
|
+
* +1 → p1 > p2
|
|
16
|
+
*/
|
|
17
|
+
compare: (p1: T, p2: T) => number;
|
|
18
|
+
equals: (p1: T, p2: T) => boolean;
|
|
19
|
+
min: (p1: T, p2: T) => T;
|
|
20
|
+
max: (p1: T, p2: T) => T;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ===========================
|
|
24
|
+
// Page request generation
|
|
25
|
+
// (shared between primitives)
|
|
26
|
+
// ===========================
|
|
27
|
+
|
|
28
|
+
export type PageRequest<Page, Result> = (page: Page) => Promise<Result>;
|
|
29
|
+
|
|
30
|
+
export function genImmediatePageRequests<
|
|
31
|
+
Page extends string | number,
|
|
32
|
+
Result,
|
|
33
|
+
>(
|
|
34
|
+
pages: Page[],
|
|
35
|
+
fetchPage: (page: Page) => Promise<Result>,
|
|
36
|
+
): PageRequest<Page, Result> {
|
|
37
|
+
const fetchedPages = Promise.all(pages.map((page) => fetchPage(page)));
|
|
38
|
+
const result = {} as Record<Page, Promise<Result>>;
|
|
39
|
+
for (let i = 0; i < pages.length; i++) {
|
|
40
|
+
result[pages[i]] = fetchedPages.then((fetched) => fetched[i]);
|
|
41
|
+
}
|
|
42
|
+
return (page: Page) => result[page];
|
|
43
|
+
}
|
|
44
|
+
export function genOnDemandPageRequests<
|
|
45
|
+
Page extends Json,
|
|
46
|
+
Result,
|
|
47
|
+
>(
|
|
48
|
+
fromPage: Page,
|
|
49
|
+
toPage: Page,
|
|
50
|
+
fetchPage: (page: Page) => Promise<Result>,
|
|
51
|
+
pageRelation: PageRelation<Page>,
|
|
52
|
+
): PageRequest<Page, Result> {
|
|
53
|
+
const promises = {} as Record<string, Promise<Result>>;
|
|
54
|
+
const pageRequest = async (
|
|
55
|
+
page: Page,
|
|
56
|
+
): Promise<Result> => {
|
|
57
|
+
if (
|
|
58
|
+
pageRelation.compare(page, fromPage) < 0 ||
|
|
59
|
+
pageRelation.compare(page, toPage) > 0
|
|
60
|
+
) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`Block out of range. ${page} not in [${fromPage}, ${toPage}]`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
const jsonKey: string = stableStringify(page)!;
|
|
66
|
+
if (promises[jsonKey] != null) {
|
|
67
|
+
return await promises[jsonKey];
|
|
68
|
+
}
|
|
69
|
+
promises[jsonKey] = fetchPage(page);
|
|
70
|
+
return await promises[jsonKey];
|
|
71
|
+
};
|
|
72
|
+
return pageRequest;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function* fetchNewestPage<Page>(
|
|
76
|
+
pageRelation: PageRelation<Page>,
|
|
77
|
+
getLatestPage: () => Operation<Page>,
|
|
78
|
+
knownLastPage: undefined | Page,
|
|
79
|
+
pollingInterval: IntervalMs,
|
|
80
|
+
): Operation<Page> {
|
|
81
|
+
return yield* retry<Page>(
|
|
82
|
+
getLatestPage,
|
|
83
|
+
(res) =>
|
|
84
|
+
knownLastPage == null
|
|
85
|
+
? true
|
|
86
|
+
: pageRelation.compare(res, knownLastPage) > 0,
|
|
87
|
+
() => sleep(pollingInterval),
|
|
88
|
+
);
|
|
89
|
+
}
|