@effectstream/sync 0.100.13 → 0.100.15
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/README.md +68 -0
- package/package.json +5 -5
- package/src/sync-protocols/avail/fetcher.ts +23 -14
- package/src/sync-protocols/bitcoin/fetcher.ts +18 -11
- package/src/sync-protocols/celestia/fetcher.ts +23 -16
- package/src/sync-protocols/evm/fetcher.ts +19 -14
- package/src/sync-protocols/midnight/fetcher.ts +19 -6
- package/src/sync-protocols/utxorpc/WatchMultiplexer.ts +14 -0
- package/src/sync-protocols/utxorpc/fetcher.ts +18 -16
- package/test/examples.test.ts +18 -0
package/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# @effectstream/sync
|
|
2
|
+
|
|
3
|
+
The blockchain-sync service inside an EffectStream node. Reads finalized
|
|
4
|
+
blocks from every chain you've configured (EVM, Bitcoin, Cardano, Midnight,
|
|
5
|
+
Avail, Celestia, NEAR…), normalizes them into a single rollup ordering,
|
|
6
|
+
and stages the inputs the state machine consumes.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
bun add @effectstream/sync
|
|
12
|
+
# or
|
|
13
|
+
npm install @effectstream/sync
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
This package pairs with [`@effectstream/runtime`](https://www.npmjs.com/package/@effectstream/runtime),
|
|
19
|
+
which boots sync as part of `start()`: it calls `genSyncProtocols(...)`
|
|
20
|
+
against the `syncProtocols` section of your
|
|
21
|
+
[`@effectstream/config`](https://www.npmjs.com/package/@effectstream/config),
|
|
22
|
+
then drives the resulting fetcher + state pairs every block. As an app
|
|
23
|
+
author you declare which protocols to sync in your config; everything
|
|
24
|
+
else runs automatically.
|
|
25
|
+
|
|
26
|
+
If you're building a new chain integration, implement the sync-protocol
|
|
27
|
+
interfaces in [`src/sync-protocols/`](https://github.com/PaimaStudios/paima-engine/tree/main/packages/node-sdk/sync/src/sync-protocols).
|
|
28
|
+
|
|
29
|
+
## Inside EffectStream
|
|
30
|
+
|
|
31
|
+
`genSyncProtocols(config)` is what the runtime calls during boot. It walks
|
|
32
|
+
the `syncProtocols` section of your `@effectstream/config`, instantiates a
|
|
33
|
+
fetcher for each chain (viem for EVM, UTXORpc for Cardano, the Midnight
|
|
34
|
+
SDK for Midnight, etc.), and writes paginated blocks into PostgreSQL
|
|
35
|
+
through `@effectstream/db`. The state machine then drains that queue.
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { genSyncProtocols } from "@effectstream/sync";
|
|
39
|
+
// inside the runtime startup path:
|
|
40
|
+
const protocols = await genSyncProtocols(config);
|
|
41
|
+
// protocols.parallelEvmRPC_fast.runOne() // poll one block
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Key exports
|
|
45
|
+
|
|
46
|
+
- `genSyncProtocols(dbConn, syncInfo)` — Effection generator that instantiates a runtime fetcher + state pair for every protocol in `syncInfo` (from `config.syncProtocols`). Called from the runtime's process-blocks loop.
|
|
47
|
+
- `AllSyncProtocols` — union type covering every supported protocol; useful when authoring config that fans out.
|
|
48
|
+
- `ChainBlock`, plus base `Fetcher`/`State` types from `sync-protocols/base/` — the wire shape per chain.
|
|
49
|
+
|
|
50
|
+
Per-chain `Fetcher` / `SyncState` classes (`EvmFetcher`,
|
|
51
|
+
`BitcoinFetcher`, `MidnightFetcher`, `AvailFetcher`, `UtxoRpcFetcher`,
|
|
52
|
+
`NtpFetcher`, `CelestiaFetcher`, `NearFetcher`, and matching `*SyncState`
|
|
53
|
+
classes) are exported but are internal to the factory wiring —
|
|
54
|
+
application code drives them through `genSyncProtocols` rather than
|
|
55
|
+
instantiating them directly. Reach for them only if you're writing a
|
|
56
|
+
custom orchestration layer.
|
|
57
|
+
|
|
58
|
+
## Examples
|
|
59
|
+
|
|
60
|
+
End-to-end sync test (boots a node, reads blocks, asserts the DB):
|
|
61
|
+
[`e2e/evm/sync/`](https://github.com/PaimaStudios/paima-engine/tree/main/e2e/evm/sync).
|
|
62
|
+
|
|
63
|
+
Runnable: [`test/examples.test.ts`](./test/examples.test.ts).
|
|
64
|
+
|
|
65
|
+
## Links
|
|
66
|
+
|
|
67
|
+
- Docs: https://effectstream.github.io/docs/packages/node/sync
|
|
68
|
+
- Source: https://github.com/PaimaStudios/paima-engine/tree/main/packages/node-sdk/sync
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effectstream/sync",
|
|
3
|
-
"version": "0.100.
|
|
3
|
+
"version": "0.100.15",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"homepage": "https://effectstream.github.io/docs/",
|
|
6
6
|
"repository": {
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
},
|
|
14
14
|
"exports": "./src/mod.ts",
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@effectstream/config": "0.100.
|
|
17
|
-
"@effectstream/db": "0.100.
|
|
18
|
-
"@effectstream/log": "0.100.
|
|
19
|
-
"@effectstream/utils": "0.100.
|
|
16
|
+
"@effectstream/config": "0.100.15",
|
|
17
|
+
"@effectstream/db": "0.100.15",
|
|
18
|
+
"@effectstream/log": "0.100.15",
|
|
19
|
+
"@effectstream/utils": "0.100.15",
|
|
20
20
|
"@midnight-ntwrk/ledger-v8": "8.0.3",
|
|
21
21
|
"@midnight-ntwrk/midnight-js-indexer-public-data-provider": "4.0.2",
|
|
22
22
|
"@midnight-ntwrk/onchain-runtime": "npm:@midnight-ntwrk/onchain-runtime-v3@3.0.0",
|
|
@@ -48,11 +48,16 @@ export class AvailFetcher extends BaseDataFetcher<
|
|
|
48
48
|
lastPage: LastPage<Page, RootPage> | undefined,
|
|
49
49
|
): Operation<DataFetched<Output, Page, RootPage>> {
|
|
50
50
|
const outputs: OutputAndCleanup<Output>[] = [];
|
|
51
|
+
const fetchAllBlocks = this.config.primitives.some(
|
|
52
|
+
(p) => p.primitive.getAllBlockHeaders,
|
|
53
|
+
);
|
|
51
54
|
console.log(
|
|
52
55
|
`[Avail] Fetching blocks from ${data.from} to ${data.to}. ${
|
|
53
56
|
data.isPresync ? "[presync]" : ""
|
|
54
57
|
}`,
|
|
55
58
|
);
|
|
59
|
+
|
|
60
|
+
let lastHeader: Output["raw"] | undefined;
|
|
56
61
|
for (let height = data.from; height <= data.to; height++) {
|
|
57
62
|
let header;
|
|
58
63
|
try {
|
|
@@ -60,26 +65,28 @@ export class AvailFetcher extends BaseDataFetcher<
|
|
|
60
65
|
this.client.getBlockHeaderFromHeight(height)
|
|
61
66
|
);
|
|
62
67
|
} catch (_e) {
|
|
63
|
-
// Likely reached the tip; stop here
|
|
64
68
|
break;
|
|
65
69
|
}
|
|
66
70
|
|
|
71
|
+
lastHeader = header;
|
|
67
72
|
const primitives = yield* this.readPrimitives(
|
|
68
73
|
height,
|
|
69
74
|
header,
|
|
70
75
|
this.config.primitives,
|
|
71
76
|
);
|
|
72
77
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
if (fetchAllBlocks || primitives.length > 0 || height === data.to) {
|
|
79
|
+
outputs.push({
|
|
80
|
+
output: {
|
|
81
|
+
raw: header,
|
|
82
|
+
primitives,
|
|
83
|
+
},
|
|
84
|
+
cleanup: () => {},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
80
87
|
}
|
|
81
88
|
|
|
82
|
-
if (
|
|
89
|
+
if (!lastHeader) {
|
|
83
90
|
if (!lastPage) {
|
|
84
91
|
throw new Error(
|
|
85
92
|
`Could not fetch any blocks from ${data.from} to ${data.to} and no previous page was found.`,
|
|
@@ -91,16 +98,18 @@ export class AvailFetcher extends BaseDataFetcher<
|
|
|
91
98
|
};
|
|
92
99
|
}
|
|
93
100
|
|
|
94
|
-
const lastOutput = outputs[outputs.length - 1].output;
|
|
95
101
|
return {
|
|
96
102
|
output: outputs,
|
|
97
103
|
lastPage: {
|
|
98
|
-
ownBlockNumber:
|
|
104
|
+
ownBlockNumber: lastHeader.number,
|
|
99
105
|
own: {
|
|
100
|
-
height:
|
|
101
|
-
hash:
|
|
106
|
+
height: lastHeader.number,
|
|
107
|
+
hash: lastHeader.hash,
|
|
102
108
|
},
|
|
103
|
-
root: rootConversion.toRootPage(
|
|
109
|
+
root: rootConversion.toRootPage({
|
|
110
|
+
raw: lastHeader,
|
|
111
|
+
primitives: [],
|
|
112
|
+
}),
|
|
104
113
|
},
|
|
105
114
|
};
|
|
106
115
|
}
|
|
@@ -111,24 +111,31 @@ export class BitcoinFetcher
|
|
|
111
111
|
);
|
|
112
112
|
const groupedByPage = this.groupByPage(primitives);
|
|
113
113
|
|
|
114
|
+
const fetchAllBlocks = this.config.primitives.some(
|
|
115
|
+
(p) => p.primitive.getAllBlockHeaders,
|
|
116
|
+
);
|
|
117
|
+
|
|
114
118
|
const outputs: OutputAndCleanup<Output>[] = [];
|
|
119
|
+
let lastBlock: BitcoinBlock | undefined;
|
|
115
120
|
for (let height = Number(data.from); height <= Number(data.to); height++) {
|
|
116
121
|
const blockHeight = height as Page;
|
|
117
122
|
const block = yield* call(() => pageFetcher(blockHeight));
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
lastBlock = block;
|
|
124
|
+
const blockPrimitives = groupedByPage[String(height)] ?? [];
|
|
125
|
+
if (fetchAllBlocks || blockPrimitives.length > 0 || height === Number(data.to)) {
|
|
126
|
+
outputs.push({
|
|
127
|
+
output: {
|
|
128
|
+
raw: block,
|
|
129
|
+
primitives: blockPrimitives,
|
|
130
|
+
blockHashes: [block.hash],
|
|
131
|
+
},
|
|
132
|
+
cleanup: () => {},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
126
135
|
}
|
|
127
|
-
|
|
128
|
-
if (!lastOutput) {
|
|
136
|
+
if (!lastBlock) {
|
|
129
137
|
throw new Error("BitcoinFetcher: no block data fetched for interval");
|
|
130
138
|
}
|
|
131
|
-
const lastBlock = lastOutput.output.raw;
|
|
132
139
|
const lastPageRef = this.asPage(Number(data.to));
|
|
133
140
|
return {
|
|
134
141
|
output: outputs,
|
|
@@ -47,6 +47,9 @@ export class CelestiaFetcher extends BaseDataFetcher<
|
|
|
47
47
|
lastPage: LastPage<Page, RootPage> | undefined,
|
|
48
48
|
): Operation<DataFetched<Output, Page, RootPage>> {
|
|
49
49
|
const outputs: OutputAndCleanup<Output>[] = [];
|
|
50
|
+
const fetchAllBlocks = this.config.primitives.some(
|
|
51
|
+
(p) => p.primitive.getAllBlockHeaders,
|
|
52
|
+
);
|
|
50
53
|
|
|
51
54
|
console.log(
|
|
52
55
|
`[Celestia] Fetching blocks from ${data.from} to ${data.to}.${
|
|
@@ -54,6 +57,7 @@ export class CelestiaFetcher extends BaseDataFetcher<
|
|
|
54
57
|
}`,
|
|
55
58
|
);
|
|
56
59
|
|
|
60
|
+
let lastFetchedOutput: Output | undefined;
|
|
57
61
|
for (let height = data.from; height <= data.to; height++) {
|
|
58
62
|
let extHeader;
|
|
59
63
|
try {
|
|
@@ -61,7 +65,6 @@ export class CelestiaFetcher extends BaseDataFetcher<
|
|
|
61
65
|
this.client.getHeaderAtHeight(height)
|
|
62
66
|
);
|
|
63
67
|
} catch (_e) {
|
|
64
|
-
// Likely reached the chain tip; stop here
|
|
65
68
|
break;
|
|
66
69
|
}
|
|
67
70
|
|
|
@@ -73,18 +76,23 @@ export class CelestiaFetcher extends BaseDataFetcher<
|
|
|
73
76
|
this.config.primitives,
|
|
74
77
|
);
|
|
75
78
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
const output: Output = {
|
|
80
|
+
timestamp: extHeader.header.time,
|
|
81
|
+
height,
|
|
82
|
+
hash: blockHash,
|
|
83
|
+
primitives,
|
|
84
|
+
};
|
|
85
|
+
lastFetchedOutput = output;
|
|
86
|
+
|
|
87
|
+
if (fetchAllBlocks || primitives.length > 0 || height === data.to) {
|
|
88
|
+
outputs.push({
|
|
89
|
+
output,
|
|
90
|
+
cleanup: () => {},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
85
93
|
}
|
|
86
94
|
|
|
87
|
-
if (
|
|
95
|
+
if (!lastFetchedOutput) {
|
|
88
96
|
if (!lastPage) {
|
|
89
97
|
throw new Error(
|
|
90
98
|
`[Celestia] Could not fetch any blocks from ${data.from} to ${data.to} and no previous page was found.`,
|
|
@@ -96,16 +104,15 @@ export class CelestiaFetcher extends BaseDataFetcher<
|
|
|
96
104
|
};
|
|
97
105
|
}
|
|
98
106
|
|
|
99
|
-
const lastOutput = outputs[outputs.length - 1].output;
|
|
100
107
|
return {
|
|
101
108
|
output: outputs,
|
|
102
109
|
lastPage: {
|
|
103
|
-
ownBlockNumber:
|
|
110
|
+
ownBlockNumber: lastFetchedOutput.height,
|
|
104
111
|
own: {
|
|
105
|
-
height:
|
|
106
|
-
hash:
|
|
112
|
+
height: lastFetchedOutput.height,
|
|
113
|
+
hash: lastFetchedOutput.hash,
|
|
107
114
|
},
|
|
108
|
-
root: rootConversion.toRootPage(
|
|
115
|
+
root: rootConversion.toRootPage(lastFetchedOutput),
|
|
109
116
|
},
|
|
110
117
|
};
|
|
111
118
|
}
|
|
@@ -25,7 +25,7 @@ import type {
|
|
|
25
25
|
import { PageSchema } from "./types.ts";
|
|
26
26
|
import {
|
|
27
27
|
fetchNewestPage,
|
|
28
|
-
|
|
28
|
+
genOnDemandPageRequests,
|
|
29
29
|
type PageRange,
|
|
30
30
|
type PageRequest,
|
|
31
31
|
} from "../base/page.ts";
|
|
@@ -62,15 +62,17 @@ export class EvmFetcher
|
|
|
62
62
|
data: Input,
|
|
63
63
|
rootConversion: RootConversion<Output, RootOutput, RootPage>,
|
|
64
64
|
): Operation<DataFetched<Output, Page, RootPage>> {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
const fetchAllBlocks = this.config.primitives.some(
|
|
66
|
+
(p) => p.primitive.getAllBlockHeaders,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const pageFetcher = genOnDemandPageRequests(
|
|
70
|
+
data.from,
|
|
71
|
+
data.to,
|
|
69
72
|
(page) => this.client.getBlock({ blockNumber: BigInt(page) }),
|
|
73
|
+
blockNumberRelation,
|
|
70
74
|
);
|
|
71
75
|
|
|
72
|
-
// TODO: if we expect multiple primitives per block
|
|
73
|
-
// it can, depending on the chain, be faster to just download the full block and parse it locally
|
|
74
76
|
const primitives = yield* this.readPrimitives(
|
|
75
77
|
data,
|
|
76
78
|
pageFetcher,
|
|
@@ -92,40 +94,43 @@ export class EvmFetcher
|
|
|
92
94
|
};
|
|
93
95
|
}),
|
|
94
96
|
),
|
|
95
|
-
// we always need
|
|
96
|
-
// so we can use it for pagination
|
|
97
|
+
// we always need the last page for pagination
|
|
97
98
|
call(() => pageFetcher(Number(data.to))),
|
|
98
99
|
],
|
|
99
100
|
);
|
|
100
101
|
|
|
101
|
-
// Build an output with all page info, as we need all the hashes to build the effectstream-block-hash
|
|
102
|
-
// All blocks are already pre-fetched in parallel by genImmediatePageRequests above
|
|
103
102
|
const allOutputs: Output[] = [];
|
|
104
103
|
for (let page = data.from; page <= data.to; page++) {
|
|
105
104
|
const _output = output.find((o) => o.raw.number === BigInt(page));
|
|
106
105
|
if (_output) {
|
|
107
106
|
allOutputs.push(_output);
|
|
108
|
-
} else {
|
|
107
|
+
} else if (fetchAllBlocks) {
|
|
109
108
|
const raw = yield* call(() => pageFetcher(page));
|
|
110
109
|
allOutputs.push({
|
|
111
110
|
raw,
|
|
112
111
|
primitives: [],
|
|
113
112
|
blockHashes: [raw.hash] as EvmBlockHash[],
|
|
114
113
|
});
|
|
114
|
+
} else if (page === data.to) {
|
|
115
|
+
allOutputs.push({
|
|
116
|
+
raw: lastPage,
|
|
117
|
+
primitives: [],
|
|
118
|
+
blockHashes: [lastPage.hash] as EvmBlockHash[],
|
|
119
|
+
});
|
|
115
120
|
}
|
|
116
121
|
}
|
|
117
122
|
|
|
118
123
|
return {
|
|
119
124
|
output: allOutputs.map((o) => ({
|
|
120
125
|
output: o,
|
|
121
|
-
cleanup: () => {},
|
|
126
|
+
cleanup: () => {},
|
|
122
127
|
})),
|
|
123
128
|
lastPage: {
|
|
124
129
|
own: Number(data.to),
|
|
125
130
|
ownBlockNumber: Number(data.to),
|
|
126
131
|
root: rootConversion.toRootPage({
|
|
127
132
|
blockHashes: [lastPage.hash] as EvmBlockHash[],
|
|
128
|
-
primitives: [],
|
|
133
|
+
primitives: [],
|
|
129
134
|
raw: lastPage,
|
|
130
135
|
}),
|
|
131
136
|
},
|
|
@@ -98,11 +98,11 @@ export class MidnightFetcher extends BaseDataFetcher<
|
|
|
98
98
|
{ length: data.to - data.from + 1 },
|
|
99
99
|
(_, i) => i + data.from,
|
|
100
100
|
);
|
|
101
|
+
const fetchAllBlocks = this.config.primitives.some(
|
|
102
|
+
(p) => p.primitive.getAllBlockHeaders,
|
|
103
|
+
);
|
|
104
|
+
|
|
101
105
|
const fetched = yield* all(
|
|
102
|
-
// The endpoint does not support range requests,
|
|
103
|
-
// so we fetch each block individually in parallel.
|
|
104
|
-
// useAbortSignal() ties each fetch's lifetime to its Effection scope,
|
|
105
|
-
// so cancellation by all() cleanly aborts the underlying HTTP request.
|
|
106
106
|
heights.map(function* (height) {
|
|
107
107
|
const signal = yield* useAbortSignal();
|
|
108
108
|
const result: MidnightGqlBlockState = yield* call(() =>
|
|
@@ -122,9 +122,22 @@ export class MidnightFetcher extends BaseDataFetcher<
|
|
|
122
122
|
};
|
|
123
123
|
}),
|
|
124
124
|
);
|
|
125
|
-
outputs.push(...fetched);
|
|
126
125
|
|
|
127
|
-
const
|
|
126
|
+
const lastFetched = fetched[fetched.length - 1];
|
|
127
|
+
if (fetchAllBlocks) {
|
|
128
|
+
outputs.push(...fetched);
|
|
129
|
+
} else {
|
|
130
|
+
for (const item of fetched) {
|
|
131
|
+
if (item.output.primitives.length > 0) {
|
|
132
|
+
outputs.push(item);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (outputs.length === 0 || outputs[outputs.length - 1] !== lastFetched) {
|
|
136
|
+
outputs.push(lastFetched);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const lastOutput = lastFetched.output;
|
|
128
141
|
return {
|
|
129
142
|
output: outputs,
|
|
130
143
|
lastPage: {
|
|
@@ -76,6 +76,20 @@ export class WatchMultiplexer {
|
|
|
76
76
|
throw new Error("No watchers configured");
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
if (this.slotOriginTimestamp === undefined) {
|
|
80
|
+
try {
|
|
81
|
+
const blocks = await this.syncClient.fetchHistory(undefined, 1);
|
|
82
|
+
if (blocks.length > 0) {
|
|
83
|
+
const blk = blocks[0].parsedBlock;
|
|
84
|
+
if (blk.timestamp && blk.header?.slot != null) {
|
|
85
|
+
this.slotOriginTimestamp = blk.timestamp - blk.header.slot;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
// Chain may not be ready yet — slotOriginTimestamp will be set on the first Apply event
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
79
93
|
const intersect = point ? [point] : [];
|
|
80
94
|
const promises: Promise<void>[] = [];
|
|
81
95
|
for (const [key, watcher] of this.watchers) {
|
|
@@ -40,34 +40,36 @@ export class UtxoRpcFetcher
|
|
|
40
40
|
);
|
|
41
41
|
const outputs: OutputAndCleanup<Output>[] = [];
|
|
42
42
|
const blocks = this.multiplexer.fetchBlocks(data.from, data.to);
|
|
43
|
+
let lastEntry: typeof blocks[number] | undefined;
|
|
43
44
|
for (const entry of blocks) {
|
|
45
|
+
lastEntry = entry;
|
|
44
46
|
const completed = entry.output;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
const primitives = this.findPrimitivesFromWatchData(completed);
|
|
48
|
+
if (primitives.length > 0) {
|
|
49
|
+
outputs.push({
|
|
50
|
+
output: {
|
|
51
|
+
blockHashes: [completed.hash],
|
|
52
|
+
raw: completed.block,
|
|
53
|
+
primitives,
|
|
54
|
+
},
|
|
55
|
+
cleanup: entry.cleanup,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
53
58
|
}
|
|
59
|
+
const lastCompleted = lastEntry!.output;
|
|
54
60
|
return {
|
|
55
61
|
output: outputs,
|
|
56
62
|
lastPage: {
|
|
57
63
|
ownBlockNumber: Number(data.to),
|
|
58
64
|
own: {
|
|
59
|
-
slot: Number(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
height: Number(
|
|
63
|
-
outputs[outputs.length - 1].output.raw.header!.height,
|
|
64
|
-
),
|
|
65
|
-
hash: outputs[outputs.length - 1].output.blockHashes[0],
|
|
65
|
+
slot: Number(lastCompleted.block.header!.slot),
|
|
66
|
+
height: Number(lastCompleted.block.header!.height),
|
|
67
|
+
hash: lastCompleted.hash,
|
|
66
68
|
},
|
|
67
69
|
root: rootConversion.toRootPage({
|
|
68
70
|
blockHashes: [],
|
|
69
71
|
primitives: [],
|
|
70
|
-
raw:
|
|
72
|
+
raw: lastCompleted.block,
|
|
71
73
|
}),
|
|
72
74
|
},
|
|
73
75
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Examples for the README — verify public exports resolve.
|
|
2
|
+
|
|
3
|
+
import { test, expect } from "bun:test";
|
|
4
|
+
import * as sync from "../src/mod.ts";
|
|
5
|
+
|
|
6
|
+
test("README: genSyncProtocols is exported as a generator factory", () => {
|
|
7
|
+
expect(typeof sync.genSyncProtocols).toBe("function");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("README: per-chain fetchers and states are exported", () => {
|
|
11
|
+
// A sampling of the protocols supported by the package.
|
|
12
|
+
expect("EvmFetcher" in sync).toBe(true);
|
|
13
|
+
expect("BitcoinFetcher" in sync).toBe(true);
|
|
14
|
+
expect("MidnightFetcher" in sync).toBe(true);
|
|
15
|
+
expect("AvailFetcher" in sync).toBe(true);
|
|
16
|
+
expect("UtxoRpcFetcher" in sync).toBe(true);
|
|
17
|
+
expect("CelestiaFetcher" in sync).toBe(true);
|
|
18
|
+
});
|