@dashevo/dapi-client 0.23.0-dev.8 → 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 CHANGED
@@ -1,8 +1,8 @@
1
1
  # DAPI Client
2
2
 
3
3
  [![NPM Version](https://img.shields.io/npm/v/@dashevo/dapi-client)](https://www.npmjs.com/package/@dashevo/dapi-client)
4
- [![Build Status](https://github.com/dashevo/platform/actions/workflows/release.yml/badge.svg)](https://github.com/dashevo/platform/actions/workflows/release.yml)
5
- [![Release Date](https://img.shields.io/github/release-date/dashevo/platform)](https://github.com/dashevo/platform/releases/latest)
4
+ [![Build Status](https://github.com/dashpay/platform/actions/workflows/release.yml/badge.svg)](https://github.com/dashpay/platform/actions/workflows/release.yml)
5
+ [![Release Date](https://img.shields.io/github/release-date/dashpay/platform)](https://github.com/dashpay/platform/releases/latest)
6
6
  [![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen)](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/dashevo/platform/tree/master/packages/dapi/lib/rpcServer/commands) and [gRPC](https://github.com/dashevo/platform/tree/master/packages/dapi/lib/grpcServer/handlers) endpoints
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://dashevo.github.io/platform/DAPI-Client/.
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/dashevo/platform/issues/new/choose) or submit PRs.
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 { SpvChain } = require('@dashevo/dash-spv');
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: 5,
17
- targetBatchSize: 100000,
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
- * @param {CoreMethodsFacade} coreMethods
44
- */
45
- setCoreMethods(coreMethods) {
46
- this.coreMethods = coreMethods;
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
- async start() {
65
- if (!this.coreMethods) {
66
- throw new Error('Core methods have not been provided. Please use "setCoreMethods"');
67
- }
68
-
69
- if (this.started) {
70
- throw new Error('BlockHeaderProvider has already been started');
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.ERROR, (e) => {
87
- this.emit(EVENTS.ERROR, e);
88
- });
89
-
90
- this.blockHeadersReader.on(BlockHeadersReader.EVENTS.HISTORICAL_DATA_OBTAINED, () => {
91
- this.blockHeadersReader.subscribeToNew(bestBlockHeight)
92
- .catch((e) => {
93
- this.emit(EVENTS.ERROR, e);
94
- });
95
- });
96
-
97
- this.blockHeadersReader.on(BlockHeadersReader.EVENTS.BLOCK_HEADERS, (headers, reject) => {
98
- try {
99
- this.spvChain.addHeaders(headers.map((header) => Buffer.from(header)));
100
- } catch (e) {
101
- if (e.message === 'Some headers are invalid') {
102
- reject(e);
103
- } else {
104
- this.emit(EVENTS.ERROR, e);
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
- this.options.fromBlockHeight,
111
- bestBlockHeight - 1,
157
+ fromBlockHeight,
158
+ toBlockHeight,
112
159
  );
113
160
 
114
- this.started = true;
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.defaultOptions = defaultOptions;
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.coreMethods = options.coreMethods;
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
- const totalAmount = toBlockHeight - fromBlockHeight + 1;
55
- if (totalAmount === 0) {
56
- return;
59
+ if (fromBlockHeight <= 0) {
60
+ throw new Error(`Invalid fromBlockHeight value: ${fromBlockHeight}`);
57
61
  }
58
62
 
59
- if (totalAmount < 0) {
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) + 1;
76
+ const startingHeight = (batchIndex * actualBatchSize) + fromBlockHeight;
93
77
  const count = Math.min(actualBatchSize, toBlockHeight - startingHeight + 1);
94
78
 
95
- const subscribeWithRetries = this.subscribeToHistoricalBatch(this.maxRetries);
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.removeAllListeners(COMMANDS.HANDLE_STREAM_RETRY);
105
- this.removeAllListeners(COMMANDS.HANDLE_STREAM_ERROR);
106
- this.removeAllListeners(COMMANDS.HANDLE_FINISHED_STREAM);
107
- this.historicalStreams.forEach((stream) => stream.destroy());
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<Stream>}
97
+ * @returns {Promise<ReconnectableStream>}
116
98
  */
117
99
  async subscribeToNew(fromBlockHeight) {
118
- const stream = await this.coreMethods.subscribeToBlockHeadersWithChainLocks({
119
- fromBlockHeight,
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 blockHeaders = data.getBlockHeaders();
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
- this.emit(EVENTS.BLOCK_HEADERS, blockHeaders.getHeadersList(), rejectHeaders);
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
- subscribeToHistoricalBatch(maxRetries = 0) {
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.coreMethods.subscribeToBlockHeadersWithChainLocks({
168
- fromBlockHeight,
169
- count,
170
- });
190
+ const stream = await this.createHistoricalSyncStream(fromBlockHeight, count);
171
191
 
172
- stream.on('data', (data) => {
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
- this.emit(EVENTS.BLOCK_HEADERS, headersList, rejectHeaders);
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.emit(COMMANDS.HANDLE_STREAM_ERROR, stream, e);
245
+ this.stopReadingHistorical();
246
+ this.emit(EVENTS.ERROR, e);
209
247
  });
210
248
  } else {
211
- this.emit(COMMANDS.HANDLE_STREAM_ERROR, stream, streamError);
249
+ this.stopReadingHistorical();
250
+ this.emit(EVENTS.ERROR, streamError);
212
251
  }
213
- });
252
+ };
214
253
 
215
- stream.on('end', () => {
216
- this.emit(COMMANDS.HANDLE_FINISHED_STREAM, stream);
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;