@dashevo/dapi-client 0.23.0-dev.9 → 0.23.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/README.md +5 -5
- package/lib/BlockHeadersProvider/BlockHeadersProvider.js +183 -50
- package/lib/BlockHeadersProvider/BlockHeadersReader.js +121 -71
- package/lib/BlockHeadersProvider/createBlockHeadersProviderFromOptions.js +31 -16
- package/lib/DAPIClient.js +0 -6
- package/lib/index.js +3 -0
- package/lib/networkConfigs.js +114 -150
- package/lib/test/bootstrap.js +6 -0
- package/lib/test/mocks/BlockHeadersWithChainLocksStreamMock.js +53 -0
- package/lib/test/mocks/mockHeadersChain.js +55 -0
- package/lib/transport/GrpcTransport/createGrpcTransportError.js +21 -26
- package/lib/transport/ReconnectableStream.js +180 -0
- package/lib/{test/utils → utils}/wait.js +0 -0
- package/package.json +10 -9
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# DAPI Client
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@dashevo/dapi-client)
|
|
4
|
-
[](https://github.com/dashpay/platform/actions/workflows/release.yml)
|
|
5
|
+
[](https://github.com/dashpay/platform/releases/latest)
|
|
6
6
|
[](https://github.com/RichardLitt/standard-readme)
|
|
7
7
|
|
|
8
8
|
Client library used to access Dash DAPI endpoints
|
|
@@ -12,7 +12,7 @@ Platform via the decentralized API ([DAPI](https://github.com/dashevo/dapi))
|
|
|
12
12
|
hosted on Dash masternodes.
|
|
13
13
|
|
|
14
14
|
- `DAPI-Client` provides automatic server (masternode) discovery using either a default seed node or a user-supplied one
|
|
15
|
-
- `DAPI-Client` maps to DAPI's [RPC](https://github.com/
|
|
15
|
+
- `DAPI-Client` maps to DAPI's [RPC](https://github.com/dashpay/platform/tree/master/packages/dapi/lib/rpcServer/commands) and [gRPC](https://github.com/dashpay/platform/tree/master/packages/dapi/lib/grpcServer/handlers) endpoints
|
|
16
16
|
|
|
17
17
|
## Table of Contents
|
|
18
18
|
- [Install](#install)
|
|
@@ -101,12 +101,12 @@ client.core.getBestBlockHash(options).then((r) => {
|
|
|
101
101
|
|
|
102
102
|
## Documentation
|
|
103
103
|
|
|
104
|
-
More extensive documentation available at https://
|
|
104
|
+
More extensive documentation available at https://dashpay.github.io/platform/DAPI-Client/.
|
|
105
105
|
|
|
106
106
|
|
|
107
107
|
## Contributing
|
|
108
108
|
|
|
109
|
-
Feel free to dive in! [Open an issue](https://github.com/
|
|
109
|
+
Feel free to dive in! [Open an issue](https://github.com/dashpay/platform/issues/new/choose) or submit PRs.
|
|
110
110
|
|
|
111
111
|
## License
|
|
112
112
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const EventEmitter = require('events');
|
|
2
|
-
const {
|
|
2
|
+
const { configure: configureDashcore } = require('@dashevo/dashcore-lib');
|
|
3
|
+
const X11 = require('wasm-x11-hash');
|
|
4
|
+
const { SpvChain, SPVError } = require('@dashevo/dash-spv');
|
|
3
5
|
|
|
4
6
|
const BlockHeadersReader = require('./BlockHeadersReader');
|
|
5
7
|
|
|
@@ -9,41 +11,53 @@ const BlockHeadersReader = require('./BlockHeadersReader');
|
|
|
9
11
|
* @property {number} [maxParallelStreams=5] max parallel streams to read historical block headers
|
|
10
12
|
* @property {number} [targetBatchSize=100000] a target batch size per stream
|
|
11
13
|
* @property {number} [maxRetries=10] max amount of retries per stream connection
|
|
12
|
-
* @property {number} [autoStart=false] auto start fetching verifying block headers
|
|
13
14
|
*/
|
|
14
15
|
const defaultOptions = {
|
|
15
16
|
network: 'testnet',
|
|
16
|
-
maxParallelStreams:
|
|
17
|
-
targetBatchSize:
|
|
17
|
+
maxParallelStreams: 10,
|
|
18
|
+
targetBatchSize: 50000,
|
|
18
19
|
fromBlockHeight: 1,
|
|
19
20
|
maxRetries: 10,
|
|
20
|
-
autoStart: false,
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
const EVENTS = {
|
|
24
24
|
ERROR: 'error',
|
|
25
|
+
HISTORICAL_SYNC_STARTED: 'HISTORICAL_SYNC_STARTED',
|
|
26
|
+
CONTINUOUS_SYNC_STARTED: 'CONTINUOUS_SYNC_STARTED',
|
|
27
|
+
CHAIN_UPDATED: 'CHAIN_UPDATED',
|
|
28
|
+
HISTORICAL_DATA_OBTAINED: 'HISTORICAL_DATA_OBTAINED',
|
|
29
|
+
STOPPED: 'STOPPED',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const STATES = {
|
|
33
|
+
IDLE: 'IDLE',
|
|
34
|
+
HISTORICAL_SYNC: 'HISTORICAL_SYNC',
|
|
35
|
+
CONTINUOUS_SYNC: 'CONTINUOUS_SYNC',
|
|
25
36
|
};
|
|
26
37
|
|
|
27
38
|
class BlockHeadersProvider extends EventEmitter {
|
|
28
39
|
/**
|
|
29
40
|
* @param {BlockHeadersProviderOptions} options
|
|
41
|
+
* @param {Function} [createHistoricalSyncStream]
|
|
42
|
+
* @param {Function} [createContinuousSyncStream]
|
|
30
43
|
*/
|
|
31
|
-
constructor(options = {}) {
|
|
44
|
+
constructor(options = {}, createHistoricalSyncStream, createContinuousSyncStream) {
|
|
32
45
|
super();
|
|
33
46
|
this.options = {
|
|
34
47
|
...defaultOptions,
|
|
35
48
|
...options,
|
|
36
49
|
};
|
|
37
50
|
|
|
38
|
-
this.spvChain = new SpvChain(this.options.network);
|
|
39
|
-
this.started = false;
|
|
40
|
-
}
|
|
51
|
+
this.spvChain = new SpvChain(this.options.network, 100);
|
|
41
52
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
this.
|
|
53
|
+
this.state = STATES.IDLE;
|
|
54
|
+
this.wasmX11Ready = false;
|
|
55
|
+
|
|
56
|
+
this.errorHandler = this.errorHandler.bind(this);
|
|
57
|
+
this.headersHandler = this.headersHandler.bind(this);
|
|
58
|
+
this.historicalDataObtainedHandler = this.historicalDataObtainedHandler.bind(this);
|
|
59
|
+
this.createHistoricalSyncStream = createHistoricalSyncStream;
|
|
60
|
+
this.createContinuousSyncStream = createContinuousSyncStream;
|
|
47
61
|
}
|
|
48
62
|
|
|
49
63
|
/**
|
|
@@ -55,23 +69,24 @@ class BlockHeadersProvider extends EventEmitter {
|
|
|
55
69
|
|
|
56
70
|
/**
|
|
57
71
|
*
|
|
58
|
-
* @param spvChain
|
|
72
|
+
* @param {SpvChain} spvChain
|
|
59
73
|
*/
|
|
60
74
|
setSpvChain(spvChain) {
|
|
61
75
|
this.spvChain = spvChain;
|
|
62
76
|
}
|
|
63
77
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
78
|
+
/**
|
|
79
|
+
* @private
|
|
80
|
+
*/
|
|
81
|
+
async init() {
|
|
82
|
+
if (!this.wasmX11Ready) {
|
|
83
|
+
const x11Hash = await X11();
|
|
84
|
+
configureDashcore({
|
|
85
|
+
x11hash: x11Hash,
|
|
86
|
+
});
|
|
87
|
+
this.wasmX11Ready = true;
|
|
71
88
|
}
|
|
72
89
|
|
|
73
|
-
const { chain: { blocksCount: bestBlockHeight } } = await this.coreMethods.getStatus();
|
|
74
|
-
|
|
75
90
|
if (!this.blockHeadersReader) {
|
|
76
91
|
this.blockHeadersReader = new BlockHeadersReader(
|
|
77
92
|
{
|
|
@@ -80,42 +95,160 @@ class BlockHeadersProvider extends EventEmitter {
|
|
|
80
95
|
targetBatchSize: this.options.targetBatchSize,
|
|
81
96
|
maxRetries: this.options.maxRetries,
|
|
82
97
|
},
|
|
98
|
+
this.createHistoricalSyncStream,
|
|
99
|
+
this.createContinuousSyncStream,
|
|
83
100
|
);
|
|
84
101
|
}
|
|
85
102
|
|
|
86
|
-
this.blockHeadersReader.on(BlockHeadersReader.EVENTS.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
103
|
+
this.blockHeadersReader.on(BlockHeadersReader.EVENTS.BLOCK_HEADERS, this.headersHandler);
|
|
104
|
+
this.blockHeadersReader.on(BlockHeadersReader.EVENTS.ERROR, this.errorHandler);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
destroyReader() {
|
|
108
|
+
this.removeReaderListeners();
|
|
109
|
+
this.blockHeadersReader = null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Initializes SPV chain with a list of headers and a known lastSyncedHeaderHeight
|
|
114
|
+
*
|
|
115
|
+
* @param headers
|
|
116
|
+
* @param lastSyncedHeaderHeight
|
|
117
|
+
*/
|
|
118
|
+
initializeWith(headers, lastSyncedHeaderHeight) {
|
|
119
|
+
const firstHeaderHeight = lastSyncedHeaderHeight - headers.length + 1;
|
|
120
|
+
this.spvChain.reset(firstHeaderHeight);
|
|
121
|
+
this.spvChain.addHeaders(headers, firstHeaderHeight);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Checks whether spv chain has header at specified height and flushes chains if not
|
|
126
|
+
*
|
|
127
|
+
* @private
|
|
128
|
+
* @param height
|
|
129
|
+
*/
|
|
130
|
+
ensureChainRoot(height) {
|
|
131
|
+
// Flush spv chain in case header at specified height was not found
|
|
132
|
+
if (!this.spvChain.hashesByHeight.has(height - 1)) {
|
|
133
|
+
this.spvChain.reset(height);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Reads historical block headers
|
|
139
|
+
*
|
|
140
|
+
* @param fromBlockHeight
|
|
141
|
+
* @param toBlockHeight
|
|
142
|
+
* @returns {Promise<void>}
|
|
143
|
+
*/
|
|
144
|
+
async readHistorical(fromBlockHeight, toBlockHeight) {
|
|
145
|
+
if (this.state !== STATES.IDLE) {
|
|
146
|
+
throw new Error(`BlockHeaderProvider can not read historical data while being in ${this.state} state.`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
await this.init();
|
|
150
|
+
|
|
151
|
+
this.ensureChainRoot(fromBlockHeight);
|
|
152
|
+
|
|
153
|
+
this.blockHeadersReader
|
|
154
|
+
.once(BlockHeadersReader.EVENTS.HISTORICAL_DATA_OBTAINED, this.historicalDataObtainedHandler);
|
|
108
155
|
|
|
109
156
|
await this.blockHeadersReader.readHistorical(
|
|
110
|
-
|
|
111
|
-
|
|
157
|
+
fromBlockHeight,
|
|
158
|
+
toBlockHeight,
|
|
112
159
|
);
|
|
113
160
|
|
|
114
|
-
this.
|
|
161
|
+
this.state = STATES.HISTORICAL_SYNC;
|
|
162
|
+
this.emit(EVENTS.HISTORICAL_SYNC_STARTED);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async startContinuousSync(fromBlockHeight) {
|
|
166
|
+
if (this.state !== STATES.IDLE) {
|
|
167
|
+
throw new Error(`BlockHeaderProvider can not sync continuous data while being in ${this.state} state.`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
await this.init();
|
|
171
|
+
this.ensureChainRoot(fromBlockHeight);
|
|
172
|
+
await this.blockHeadersReader.subscribeToNew(fromBlockHeight);
|
|
173
|
+
this.state = STATES.CONTINUOUS_SYNC;
|
|
174
|
+
this.emit(EVENTS.CONTINUOUS_SYNC_STARTED);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async stop() {
|
|
178
|
+
if (this.state === STATES.IDLE) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (this.state === STATES.HISTORICAL_SYNC) {
|
|
183
|
+
this.blockHeadersReader.stopReadingHistorical();
|
|
184
|
+
} else if (this.state === STATES.CONTINUOUS_SYNC) {
|
|
185
|
+
this.blockHeadersReader.unsubscribeFromNew();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this.destroyReader();
|
|
189
|
+
|
|
190
|
+
this.state = STATES.IDLE;
|
|
191
|
+
|
|
192
|
+
this.emit(EVENTS.STOPPED);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
errorHandler(e) {
|
|
196
|
+
this.destroyReader();
|
|
197
|
+
this.emit(EVENTS.ERROR, e);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* @private
|
|
202
|
+
* @param headersData
|
|
203
|
+
* @param reject
|
|
204
|
+
*/
|
|
205
|
+
headersHandler(headersData, reject) {
|
|
206
|
+
const { headers, headHeight } = headersData;
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const headersAdded = this.spvChain.addHeaders(headers, headHeight);
|
|
210
|
+
|
|
211
|
+
if (headersAdded.length) {
|
|
212
|
+
// Calculate amount of removed headers in order to properly adjust head height
|
|
213
|
+
// TODO(spv): move this logic to SpvChain?
|
|
214
|
+
const difference = headers.length - headersAdded.length;
|
|
215
|
+
this.emit(EVENTS.CHAIN_UPDATED, headersAdded, headHeight + difference);
|
|
216
|
+
}
|
|
217
|
+
} catch (e) {
|
|
218
|
+
if (e instanceof SPVError) {
|
|
219
|
+
reject(e);
|
|
220
|
+
} else {
|
|
221
|
+
this.errorHandler(e);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
historicalDataObtainedHandler() {
|
|
227
|
+
try {
|
|
228
|
+
this.spvChain.validate();
|
|
229
|
+
this.emit(EVENTS.HISTORICAL_DATA_OBTAINED);
|
|
230
|
+
this.removeReaderListeners();
|
|
231
|
+
} catch (e) {
|
|
232
|
+
this.errorHandler(e);
|
|
233
|
+
} finally {
|
|
234
|
+
this.state = STATES.IDLE;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
removeReaderListeners() {
|
|
239
|
+
this.blockHeadersReader.removeListener(BlockHeadersReader.EVENTS.ERROR, this.errorHandler);
|
|
240
|
+
this.blockHeadersReader
|
|
241
|
+
.removeListener(BlockHeadersReader.EVENTS.BLOCK_HEADERS, this.headersHandler);
|
|
242
|
+
this.blockHeadersReader
|
|
243
|
+
.removeListener(
|
|
244
|
+
BlockHeadersReader.EVENTS.HISTORICAL_DATA_OBTAINED,
|
|
245
|
+
this.historicalDataObtainedHandler,
|
|
246
|
+
);
|
|
115
247
|
}
|
|
116
248
|
}
|
|
117
249
|
|
|
118
250
|
BlockHeadersProvider.EVENTS = EVENTS;
|
|
119
|
-
BlockHeadersProvider.
|
|
251
|
+
BlockHeadersProvider.STATES = STATES;
|
|
252
|
+
BlockHeadersProvider.defaultOptions = { ...defaultOptions };
|
|
120
253
|
|
|
121
254
|
module.exports = BlockHeadersProvider;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
const { EventEmitter } = require('events');
|
|
2
|
+
const { BlockHeader } = require('@dashevo/dashcore-lib');
|
|
3
|
+
const GrpcErrorCodes = require('@dashevo/grpc-common/lib/server/error/GrpcErrorCodes');
|
|
2
4
|
|
|
3
5
|
const EVENTS = {
|
|
4
6
|
BLOCK_HEADERS: 'BLOCK_HEADERS',
|
|
@@ -6,15 +8,8 @@ const EVENTS = {
|
|
|
6
8
|
ERROR: 'error',
|
|
7
9
|
};
|
|
8
10
|
|
|
9
|
-
const COMMANDS = {
|
|
10
|
-
HANDLE_FINISHED_STREAM: 'HANDLE_FINISHED_STREAM',
|
|
11
|
-
HANDLE_STREAM_RETRY: 'HANDLE_STREAM_RETRY',
|
|
12
|
-
HANDLE_STREAM_ERROR: 'HANDLE_STREAM_ERROR',
|
|
13
|
-
};
|
|
14
|
-
|
|
15
11
|
/**
|
|
16
12
|
* @typedef BlockHeadersReaderOptions
|
|
17
|
-
* @property {CoreMethodsFacade} [coreMethods]
|
|
18
13
|
* @property {number} [maxParallelStreams]
|
|
19
14
|
* @property {number} [targetBatchSize]
|
|
20
15
|
* @property {number} [maxRetries]
|
|
@@ -23,10 +18,13 @@ const COMMANDS = {
|
|
|
23
18
|
class BlockHeadersReader extends EventEmitter {
|
|
24
19
|
/**
|
|
25
20
|
* @param {BlockHeadersReaderOptions} options
|
|
21
|
+
* @param createHistoricalSyncStream
|
|
22
|
+
* @param createContinuousSyncStream
|
|
26
23
|
*/
|
|
27
|
-
constructor(options = {}) {
|
|
24
|
+
constructor(options = {}, createHistoricalSyncStream, createContinuousSyncStream) {
|
|
28
25
|
super();
|
|
29
|
-
this.
|
|
26
|
+
this.createHistoricalSyncStream = createHistoricalSyncStream;
|
|
27
|
+
this.createContinuousSyncStream = createContinuousSyncStream;
|
|
30
28
|
this.maxParallelStreams = options.maxParallelStreams;
|
|
31
29
|
this.targetBatchSize = options.targetBatchSize;
|
|
32
30
|
this.maxRetries = options.maxRetries;
|
|
@@ -34,9 +32,16 @@ class BlockHeadersReader extends EventEmitter {
|
|
|
34
32
|
/**
|
|
35
33
|
* Holds references to the historical streams
|
|
36
34
|
*
|
|
37
|
-
* @type {
|
|
35
|
+
* @type {Stream[]}
|
|
38
36
|
*/
|
|
39
37
|
this.historicalStreams = [];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Holds reference to the continuous sync stream
|
|
41
|
+
*
|
|
42
|
+
* @type {Stream}
|
|
43
|
+
*/
|
|
44
|
+
this.continuousSyncStream = null;
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
/**
|
|
@@ -48,52 +53,30 @@ class BlockHeadersReader extends EventEmitter {
|
|
|
48
53
|
*/
|
|
49
54
|
async readHistorical(fromBlockHeight, toBlockHeight) {
|
|
50
55
|
if (this.historicalStreams.length) {
|
|
51
|
-
throw new Error('Historical streams are already running');
|
|
56
|
+
throw new Error('Historical streams are already running. Please stop them first.');
|
|
52
57
|
}
|
|
53
58
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return;
|
|
59
|
+
if (fromBlockHeight <= 0) {
|
|
60
|
+
throw new Error(`Invalid fromBlockHeight value: ${fromBlockHeight}`);
|
|
57
61
|
}
|
|
58
62
|
|
|
59
|
-
|
|
63
|
+
const totalAmount = toBlockHeight - fromBlockHeight + 1;
|
|
64
|
+
if (totalAmount <= 0) {
|
|
60
65
|
throw new Error(`Invalid total amount of headers to read: ${totalAmount}`);
|
|
61
66
|
}
|
|
62
67
|
|
|
63
|
-
// Resubscribe to the stream in case of error, and replace the stream in the array
|
|
64
|
-
this.on(COMMANDS.HANDLE_STREAM_RETRY, (oldStream, newStream) => {
|
|
65
|
-
const index = this.historicalStreams.indexOf(oldStream);
|
|
66
|
-
this.historicalStreams[index] = newStream;
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
// Remove stream from the array in case of error
|
|
70
|
-
this.on(COMMANDS.HANDLE_STREAM_ERROR, (stream, e) => {
|
|
71
|
-
const index = this.historicalStreams.indexOf(stream);
|
|
72
|
-
this.historicalStreams.splice(index, 1);
|
|
73
|
-
this.emit(EVENTS.ERROR, e);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
// Remove finished stream from the array and emit HISTORICAL_DATA_OBTAINED event
|
|
77
|
-
this.on(COMMANDS.HANDLE_FINISHED_STREAM, (stream) => {
|
|
78
|
-
const index = this.historicalStreams.indexOf(stream);
|
|
79
|
-
this.historicalStreams.splice(index, 1);
|
|
80
|
-
if (this.historicalStreams.length === 0) {
|
|
81
|
-
this.emit(EVENTS.HISTORICAL_DATA_OBTAINED);
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
|
|
85
68
|
const numStreams = Math.min(
|
|
86
69
|
Math.max(Math.round(totalAmount / this.targetBatchSize), 1),
|
|
87
70
|
this.maxParallelStreams,
|
|
88
71
|
);
|
|
89
72
|
|
|
90
73
|
const actualBatchSize = Math.ceil(totalAmount / numStreams);
|
|
74
|
+
|
|
91
75
|
for (let batchIndex = 0; batchIndex < numStreams; batchIndex += 1) {
|
|
92
|
-
const startingHeight = (batchIndex * actualBatchSize) +
|
|
76
|
+
const startingHeight = (batchIndex * actualBatchSize) + fromBlockHeight;
|
|
93
77
|
const count = Math.min(actualBatchSize, toBlockHeight - startingHeight + 1);
|
|
94
78
|
|
|
95
|
-
const subscribeWithRetries = this.
|
|
96
|
-
|
|
79
|
+
const subscribeWithRetries = this.createSubscribeToHistoricalBatch(this.maxRetries);
|
|
97
80
|
// eslint-disable-next-line no-await-in-loop
|
|
98
81
|
const stream = await subscribeWithRetries(startingHeight, count);
|
|
99
82
|
this.historicalStreams.push(stream);
|
|
@@ -101,28 +84,42 @@ class BlockHeadersReader extends EventEmitter {
|
|
|
101
84
|
}
|
|
102
85
|
|
|
103
86
|
stopReadingHistorical() {
|
|
104
|
-
this.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
this.historicalStreams = [];
|
|
87
|
+
while (this.historicalStreams.length) {
|
|
88
|
+
const stream = this.historicalStreams.shift();
|
|
89
|
+
stream.cancel();
|
|
90
|
+
}
|
|
109
91
|
}
|
|
110
92
|
|
|
111
93
|
/**
|
|
112
94
|
* Subscribes to continuously arriving block headers
|
|
113
95
|
*
|
|
114
96
|
* @param {number} fromBlockHeight
|
|
115
|
-
* @returns {Promise<
|
|
97
|
+
* @returns {Promise<ReconnectableStream>}
|
|
116
98
|
*/
|
|
117
99
|
async subscribeToNew(fromBlockHeight) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
100
|
+
if (this.continuousSyncStream) {
|
|
101
|
+
throw new Error('Continuous sync has already been started');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (typeof fromBlockHeight !== 'number' || fromBlockHeight < 1) {
|
|
105
|
+
throw new Error(`Invalid fromBlockHeight: ${fromBlockHeight}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let lastKnownChainHeight = fromBlockHeight - 1;
|
|
109
|
+
|
|
110
|
+
const stream = await this.createContinuousSyncStream(fromBlockHeight);
|
|
121
111
|
|
|
122
112
|
stream.on('data', (data) => {
|
|
123
|
-
const
|
|
113
|
+
const blockHeadersResponse = data.getBlockHeaders();
|
|
114
|
+
|
|
115
|
+
if (blockHeadersResponse) {
|
|
116
|
+
const rawHeaders = blockHeadersResponse.getHeadersList();
|
|
117
|
+
|
|
118
|
+
const headers = rawHeaders.map((header) => new BlockHeader(Buffer.from(header)));
|
|
119
|
+
|
|
120
|
+
lastKnownChainHeight += headers.length;
|
|
121
|
+
const batchHeadHeight = lastKnownChainHeight - headers.length + 1;
|
|
124
122
|
|
|
125
|
-
if (blockHeaders) {
|
|
126
123
|
/**
|
|
127
124
|
* Kills stream in case of deliberate rejection from the outside
|
|
128
125
|
*
|
|
@@ -131,18 +128,44 @@ class BlockHeadersReader extends EventEmitter {
|
|
|
131
128
|
const rejectHeaders = (e) => {
|
|
132
129
|
stream.destroy(e);
|
|
133
130
|
};
|
|
134
|
-
|
|
135
|
-
|
|
131
|
+
this.emit(EVENTS.BLOCK_HEADERS, {
|
|
132
|
+
headers,
|
|
133
|
+
headHeight: batchHeadHeight,
|
|
134
|
+
}, rejectHeaders);
|
|
136
135
|
}
|
|
137
136
|
});
|
|
138
137
|
|
|
138
|
+
stream.on('beforeReconnect', (updateArguments) => {
|
|
139
|
+
updateArguments({
|
|
140
|
+
fromBlockHeight: lastKnownChainHeight,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
lastKnownChainHeight -= 1;
|
|
144
|
+
});
|
|
145
|
+
|
|
139
146
|
stream.on('error', (e) => {
|
|
147
|
+
this.continuousSyncStream = null;
|
|
148
|
+
if (e.code === GrpcErrorCodes.CANCELLED) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
140
151
|
this.emit(EVENTS.ERROR, e);
|
|
141
152
|
});
|
|
142
153
|
|
|
154
|
+
stream.on('end', () => {
|
|
155
|
+
this.continuousSyncStream = null;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
this.continuousSyncStream = stream;
|
|
143
159
|
return stream;
|
|
144
160
|
}
|
|
145
161
|
|
|
162
|
+
unsubscribeFromNew() {
|
|
163
|
+
if (this.continuousSyncStream) {
|
|
164
|
+
this.continuousSyncStream.cancel();
|
|
165
|
+
this.continuousSyncStream = null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
146
169
|
/**
|
|
147
170
|
* A HOF that returns a function to subscribe to historical block headers and chain locks
|
|
148
171
|
* and handles retry logic
|
|
@@ -151,7 +174,7 @@ class BlockHeadersReader extends EventEmitter {
|
|
|
151
174
|
* @param {number} [maxRetries=0] - maximum amount of retries
|
|
152
175
|
* @returns {function(*, *): Promise<Stream>}
|
|
153
176
|
*/
|
|
154
|
-
|
|
177
|
+
createSubscribeToHistoricalBatch(maxRetries = 0) {
|
|
155
178
|
let currentRetries = 0;
|
|
156
179
|
|
|
157
180
|
/**
|
|
@@ -164,16 +187,14 @@ class BlockHeadersReader extends EventEmitter {
|
|
|
164
187
|
const subscribeWithRetries = async (fromBlockHeight, count) => {
|
|
165
188
|
let headersObtained = 0;
|
|
166
189
|
|
|
167
|
-
const stream = await this.
|
|
168
|
-
fromBlockHeight,
|
|
169
|
-
count,
|
|
170
|
-
});
|
|
190
|
+
const stream = await this.createHistoricalSyncStream(fromBlockHeight, count);
|
|
171
191
|
|
|
172
|
-
|
|
192
|
+
const dataHandler = (data) => {
|
|
173
193
|
const blockHeaders = data.getBlockHeaders();
|
|
174
194
|
|
|
175
195
|
if (blockHeaders) {
|
|
176
|
-
const headersList = blockHeaders.getHeadersList()
|
|
196
|
+
const headersList = blockHeaders.getHeadersList()
|
|
197
|
+
.map((header) => new BlockHeader(Buffer.from(header)));
|
|
177
198
|
|
|
178
199
|
let rejected = false;
|
|
179
200
|
|
|
@@ -187,34 +208,64 @@ class BlockHeadersReader extends EventEmitter {
|
|
|
187
208
|
stream.destroy(e);
|
|
188
209
|
};
|
|
189
210
|
|
|
190
|
-
|
|
211
|
+
const batchHeadHeight = fromBlockHeight + headersObtained;
|
|
212
|
+
this.emit(EVENTS.BLOCK_HEADERS, {
|
|
213
|
+
headers: headersList,
|
|
214
|
+
headHeight: batchHeadHeight,
|
|
215
|
+
}, rejectHeaders);
|
|
191
216
|
|
|
192
217
|
if (!rejected) {
|
|
193
218
|
headersObtained += headersList.length;
|
|
194
219
|
}
|
|
195
220
|
}
|
|
196
|
-
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const errorHandler = (streamError) => {
|
|
224
|
+
if (streamError.code === GrpcErrorCodes.CANCELLED) {
|
|
225
|
+
// TODO: consider reworking with COMMANDS instead
|
|
226
|
+
// of producing a side effect that alters class state
|
|
227
|
+
const index = this.historicalStreams.indexOf(stream);
|
|
228
|
+
if (index >= 0) {
|
|
229
|
+
this.historicalStreams.splice(index, 1);
|
|
230
|
+
}
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
197
233
|
|
|
198
|
-
stream.on('error', (streamError) => {
|
|
199
234
|
if (currentRetries < maxRetries) {
|
|
200
235
|
const newFromBlockHeight = fromBlockHeight + headersObtained;
|
|
201
236
|
const newCount = count - headersObtained;
|
|
202
237
|
|
|
238
|
+
// TODO: do not retry in case newCount is zero
|
|
203
239
|
subscribeWithRetries(newFromBlockHeight, newCount)
|
|
204
240
|
.then((newStream) => {
|
|
241
|
+
const index = this.historicalStreams.indexOf(stream);
|
|
242
|
+
this.historicalStreams[index] = newStream;
|
|
205
243
|
currentRetries += 1;
|
|
206
|
-
this.emit(COMMANDS.HANDLE_STREAM_RETRY, stream, newStream);
|
|
207
244
|
}).catch((e) => {
|
|
208
|
-
this.
|
|
245
|
+
this.stopReadingHistorical();
|
|
246
|
+
this.emit(EVENTS.ERROR, e);
|
|
209
247
|
});
|
|
210
248
|
} else {
|
|
211
|
-
this.
|
|
249
|
+
this.stopReadingHistorical();
|
|
250
|
+
this.emit(EVENTS.ERROR, streamError);
|
|
212
251
|
}
|
|
213
|
-
}
|
|
252
|
+
};
|
|
214
253
|
|
|
215
|
-
|
|
216
|
-
this.
|
|
217
|
-
|
|
254
|
+
const endHandler = () => {
|
|
255
|
+
const index = this.historicalStreams.indexOf(stream);
|
|
256
|
+
this.historicalStreams.splice(index, 1);
|
|
257
|
+
if (this.historicalStreams.length === 0) {
|
|
258
|
+
this.emit(EVENTS.HISTORICAL_DATA_OBTAINED);
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// TODO: consider reworking with "pasued" mode to
|
|
263
|
+
// control the data flow manually because SPV verification is
|
|
264
|
+
// resource intense and backpressure could be a problem
|
|
265
|
+
// also the whole thing severely affects UI responsiveness
|
|
266
|
+
stream.on('data', dataHandler);
|
|
267
|
+
stream.on('error', errorHandler);
|
|
268
|
+
stream.on('end', endHandler);
|
|
218
269
|
|
|
219
270
|
return stream;
|
|
220
271
|
};
|
|
@@ -224,6 +275,5 @@ class BlockHeadersReader extends EventEmitter {
|
|
|
224
275
|
}
|
|
225
276
|
|
|
226
277
|
BlockHeadersReader.EVENTS = EVENTS;
|
|
227
|
-
BlockHeadersReader.COMMANDS = COMMANDS;
|
|
228
278
|
|
|
229
279
|
module.exports = BlockHeadersReader;
|