@dashevo/dapi-client 0.23.0 → 0.24.0-dev.10
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/lib/BlockHeadersProvider/BlockHeadersProvider.js +4 -0
- package/lib/BlockHeadersProvider/BlockHeadersReader.js +80 -48
- package/lib/dapiAddressProvider/DAPIAddress.js +27 -2
- package/lib/dapiAddressProvider/ListDAPIAddressProvider.js +3 -1
- package/lib/methods/platform/response/Metadata.js +21 -1
- package/lib/methods/platform/response/Proof.js +16 -5
- package/lib/test/fixtures/getMetadataFixture.js +9 -1
- package/lib/test/fixtures/getProofFixture.js +3 -2
- package/lib/test/mocks/BlockHeadersWithChainLocksStreamMock.js +9 -8
- package/lib/transport/GrpcTransport/GrpcTransport.js +5 -2
- package/lib/transport/GrpcTransport/createGrpcTransportError.js +2 -10
- package/lib/transport/JsonRpcTransport/JsonRpcTransport.js +2 -0
- package/lib/transport/JsonRpcTransport/requestJsonRpc.js +19 -4
- package/lib/transport/ReconnectableStream.js +148 -44
- package/package.json +7 -7
|
@@ -205,6 +205,10 @@ class BlockHeadersProvider extends EventEmitter {
|
|
|
205
205
|
headersHandler(headersData, reject) {
|
|
206
206
|
const { headers, headHeight } = headersData;
|
|
207
207
|
|
|
208
|
+
// TODO: simulate headers rejection and debug
|
|
209
|
+
// BlockHeadersReader might not correctly set startBlockHeight
|
|
210
|
+
// which results in orphan chunks in SPV
|
|
211
|
+
|
|
208
212
|
try {
|
|
209
213
|
const headersAdded = this.spvChain.addHeaders(headers, headHeight);
|
|
210
214
|
|
|
@@ -86,7 +86,7 @@ class BlockHeadersReader extends EventEmitter {
|
|
|
86
86
|
stopReadingHistorical() {
|
|
87
87
|
while (this.historicalStreams.length) {
|
|
88
88
|
const stream = this.historicalStreams.shift();
|
|
89
|
-
|
|
89
|
+
this.cancelStream(stream);
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
@@ -105,11 +105,20 @@ class BlockHeadersReader extends EventEmitter {
|
|
|
105
105
|
throw new Error(`Invalid fromBlockHeight: ${fromBlockHeight}`);
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
// We don't know yet whether we already have header at fromBlockHeight
|
|
108
109
|
let lastKnownChainHeight = fromBlockHeight - 1;
|
|
109
110
|
|
|
110
111
|
const stream = await this.createContinuousSyncStream(fromBlockHeight);
|
|
111
112
|
|
|
112
|
-
|
|
113
|
+
const errorHandler = (e) => {
|
|
114
|
+
this.continuousSyncStream = null;
|
|
115
|
+
if (e.code === GrpcErrorCodes.CANCELLED) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
this.emit(EVENTS.ERROR, e);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const dataHandler = (data) => {
|
|
113
122
|
const blockHeadersResponse = data.getBlockHeaders();
|
|
114
123
|
|
|
115
124
|
if (blockHeadersResponse) {
|
|
@@ -125,35 +134,42 @@ class BlockHeadersReader extends EventEmitter {
|
|
|
125
134
|
*
|
|
126
135
|
* @param e
|
|
127
136
|
*/
|
|
128
|
-
const rejectHeaders = (e) => {
|
|
129
|
-
|
|
137
|
+
const rejectHeaders = async (e) => {
|
|
138
|
+
// Don't use cancelStream there because it's going to unsubscribe from events
|
|
139
|
+
stream.cancel();
|
|
140
|
+
stream.retryOnError(e);
|
|
130
141
|
};
|
|
142
|
+
|
|
131
143
|
this.emit(EVENTS.BLOCK_HEADERS, {
|
|
132
144
|
headers,
|
|
133
145
|
headHeight: batchHeadHeight,
|
|
134
146
|
}, rejectHeaders);
|
|
135
147
|
}
|
|
136
|
-
}
|
|
148
|
+
};
|
|
137
149
|
|
|
138
|
-
|
|
150
|
+
const beforeReconnectHandler = (updateArguments) => {
|
|
139
151
|
updateArguments({
|
|
140
152
|
fromBlockHeight: lastKnownChainHeight,
|
|
153
|
+
count: 0,
|
|
141
154
|
});
|
|
142
155
|
|
|
143
156
|
lastKnownChainHeight -= 1;
|
|
144
|
-
}
|
|
157
|
+
};
|
|
145
158
|
|
|
146
|
-
|
|
159
|
+
const endHandler = () => {
|
|
147
160
|
this.continuousSyncStream = null;
|
|
148
|
-
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
this.emit(EVENTS.ERROR, e);
|
|
152
|
-
});
|
|
161
|
+
};
|
|
153
162
|
|
|
154
|
-
stream.on('
|
|
155
|
-
|
|
156
|
-
|
|
163
|
+
stream.on('data', dataHandler);
|
|
164
|
+
stream.on('beforeReconnect', beforeReconnectHandler);
|
|
165
|
+
stream.on('error', errorHandler);
|
|
166
|
+
stream.on('end', endHandler);
|
|
167
|
+
|
|
168
|
+
stream.removeAllListeners = () => {
|
|
169
|
+
stream.removeListener('data', dataHandler);
|
|
170
|
+
stream.removeListener('end', endHandler);
|
|
171
|
+
stream.removeListener('beforeReconnect', beforeReconnectHandler);
|
|
172
|
+
};
|
|
157
173
|
|
|
158
174
|
this.continuousSyncStream = stream;
|
|
159
175
|
return stream;
|
|
@@ -161,11 +177,13 @@ class BlockHeadersReader extends EventEmitter {
|
|
|
161
177
|
|
|
162
178
|
unsubscribeFromNew() {
|
|
163
179
|
if (this.continuousSyncStream) {
|
|
164
|
-
this.continuousSyncStream
|
|
180
|
+
this.cancelStream(this.continuousSyncStream);
|
|
165
181
|
this.continuousSyncStream = null;
|
|
166
182
|
}
|
|
167
183
|
}
|
|
168
184
|
|
|
185
|
+
// TODO: refactor whole thing with ReconnectableStream that supports
|
|
186
|
+
// retry on error logic
|
|
169
187
|
/**
|
|
170
188
|
* A HOF that returns a function to subscribe to historical block headers and chain locks
|
|
171
189
|
* and handles retry logic
|
|
@@ -189,37 +207,6 @@ class BlockHeadersReader extends EventEmitter {
|
|
|
189
207
|
|
|
190
208
|
const stream = await this.createHistoricalSyncStream(fromBlockHeight, count);
|
|
191
209
|
|
|
192
|
-
const dataHandler = (data) => {
|
|
193
|
-
const blockHeaders = data.getBlockHeaders();
|
|
194
|
-
|
|
195
|
-
if (blockHeaders) {
|
|
196
|
-
const headersList = blockHeaders.getHeadersList()
|
|
197
|
-
.map((header) => new BlockHeader(Buffer.from(header)));
|
|
198
|
-
|
|
199
|
-
let rejected = false;
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Kills stream in case of deliberate rejection from the outside
|
|
203
|
-
*
|
|
204
|
-
* @param e
|
|
205
|
-
*/
|
|
206
|
-
const rejectHeaders = (e) => {
|
|
207
|
-
rejected = true;
|
|
208
|
-
stream.destroy(e);
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
const batchHeadHeight = fromBlockHeight + headersObtained;
|
|
212
|
-
this.emit(EVENTS.BLOCK_HEADERS, {
|
|
213
|
-
headers: headersList,
|
|
214
|
-
headHeight: batchHeadHeight,
|
|
215
|
-
}, rejectHeaders);
|
|
216
|
-
|
|
217
|
-
if (!rejected) {
|
|
218
|
-
headersObtained += headersList.length;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
};
|
|
222
|
-
|
|
223
210
|
const errorHandler = (streamError) => {
|
|
224
211
|
if (streamError.code === GrpcErrorCodes.CANCELLED) {
|
|
225
212
|
// TODO: consider reworking with COMMANDS instead
|
|
@@ -251,6 +238,40 @@ class BlockHeadersReader extends EventEmitter {
|
|
|
251
238
|
}
|
|
252
239
|
};
|
|
253
240
|
|
|
241
|
+
const dataHandler = (data) => {
|
|
242
|
+
const blockHeaders = data.getBlockHeaders();
|
|
243
|
+
|
|
244
|
+
if (blockHeaders) {
|
|
245
|
+
const headersList = blockHeaders.getHeadersList()
|
|
246
|
+
.map((header) => new BlockHeader(Buffer.from(header)));
|
|
247
|
+
|
|
248
|
+
let rejected = false;
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Kills stream in case of deliberate rejection from the outside
|
|
252
|
+
*
|
|
253
|
+
* @param e
|
|
254
|
+
*/
|
|
255
|
+
const rejectHeaders = (e) => {
|
|
256
|
+
rejected = true;
|
|
257
|
+
// Cancel stream and unsubscribe from all data events
|
|
258
|
+
// because they are going th be re-created in errorHandler
|
|
259
|
+
this.cancelStream(stream);
|
|
260
|
+
errorHandler(e);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const batchHeadHeight = fromBlockHeight + headersObtained;
|
|
264
|
+
this.emit(EVENTS.BLOCK_HEADERS, {
|
|
265
|
+
headers: headersList,
|
|
266
|
+
headHeight: batchHeadHeight,
|
|
267
|
+
}, rejectHeaders);
|
|
268
|
+
|
|
269
|
+
if (!rejected) {
|
|
270
|
+
headersObtained += headersList.length;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
254
275
|
const endHandler = () => {
|
|
255
276
|
const index = this.historicalStreams.indexOf(stream);
|
|
256
277
|
this.historicalStreams.splice(index, 1);
|
|
@@ -267,11 +288,22 @@ class BlockHeadersReader extends EventEmitter {
|
|
|
267
288
|
stream.on('error', errorHandler);
|
|
268
289
|
stream.on('end', endHandler);
|
|
269
290
|
|
|
291
|
+
stream.removeAllListeners = () => {
|
|
292
|
+
stream.removeListener('data', dataHandler);
|
|
293
|
+
stream.removeListener('end', endHandler);
|
|
294
|
+
};
|
|
295
|
+
|
|
270
296
|
return stream;
|
|
271
297
|
};
|
|
272
298
|
|
|
273
299
|
return subscribeWithRetries;
|
|
274
300
|
}
|
|
301
|
+
|
|
302
|
+
// eslint-disable-next-line class-methods-use-this
|
|
303
|
+
cancelStream(stream) {
|
|
304
|
+
stream.removeAllListeners();
|
|
305
|
+
stream.cancel();
|
|
306
|
+
}
|
|
275
307
|
}
|
|
276
308
|
|
|
277
309
|
BlockHeadersReader.EVENTS = EVENTS;
|
|
@@ -10,13 +10,15 @@ class DAPIAddress {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
if (typeof address === 'string') {
|
|
13
|
-
const [host, httpPort, grpcPort] = address.split(':');
|
|
13
|
+
const [host, httpPort, grpcPort, ssl] = address.split(':');
|
|
14
14
|
|
|
15
15
|
// eslint-disable-next-line no-param-reassign
|
|
16
16
|
address = {
|
|
17
17
|
host,
|
|
18
18
|
httpPort: httpPort ? parseInt(httpPort, 10) : DAPIAddress.DEFAULT_HTTP_PORT,
|
|
19
19
|
grpcPort: grpcPort ? parseInt(grpcPort, 10) : DAPIAddress.DEFAULT_GRPC_PORT,
|
|
20
|
+
protocol: ssl === 'no-ssl' ? 'http' : DAPIAddress.DEFAULT_PROTOCOL,
|
|
21
|
+
allowSelfSignedCertificate: ssl === 'self-signed',
|
|
20
22
|
};
|
|
21
23
|
}
|
|
22
24
|
|
|
@@ -24,15 +26,26 @@ class DAPIAddress {
|
|
|
24
26
|
throw new DAPIAddressHostMissingError();
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
this.protocol = address.protocol || DAPIAddress.DEFAULT_PROTOCOL;
|
|
27
30
|
this.host = address.host;
|
|
28
31
|
this.httpPort = address.httpPort || DAPIAddress.DEFAULT_HTTP_PORT;
|
|
29
32
|
this.grpcPort = address.grpcPort || DAPIAddress.DEFAULT_GRPC_PORT;
|
|
30
33
|
this.proRegTxHash = address.proRegTxHash;
|
|
34
|
+
this.allowSelfSignedCertificate = address.allowSelfSignedCertificate || false;
|
|
31
35
|
|
|
32
36
|
this.banCount = 0;
|
|
33
37
|
this.banStartTime = undefined;
|
|
34
38
|
}
|
|
35
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Get protocol
|
|
42
|
+
*
|
|
43
|
+
* @returns {string}
|
|
44
|
+
*/
|
|
45
|
+
getProtocol() {
|
|
46
|
+
return this.protocol;
|
|
47
|
+
}
|
|
48
|
+
|
|
36
49
|
/**
|
|
37
50
|
* Get host
|
|
38
51
|
*
|
|
@@ -150,6 +163,13 @@ class DAPIAddress {
|
|
|
150
163
|
return this.banCount > 0;
|
|
151
164
|
}
|
|
152
165
|
|
|
166
|
+
/**
|
|
167
|
+
* @returns {boolean}
|
|
168
|
+
*/
|
|
169
|
+
isSelfSignedCertificateAllowed() {
|
|
170
|
+
return this.allowSelfSignedCertificate;
|
|
171
|
+
}
|
|
172
|
+
|
|
153
173
|
/**
|
|
154
174
|
* Return DAPIAddress as plain object
|
|
155
175
|
*
|
|
@@ -157,27 +177,32 @@ class DAPIAddress {
|
|
|
157
177
|
*/
|
|
158
178
|
toJSON() {
|
|
159
179
|
return {
|
|
180
|
+
protocol: this.getProtocol(),
|
|
160
181
|
host: this.getHost(),
|
|
161
182
|
httpPort: this.getHttpPort(),
|
|
162
183
|
grpcPort: this.getGrpcPort(),
|
|
163
184
|
proRegTxHash: this.getProRegTxHash(),
|
|
185
|
+
allowSelfSignedCertificate: this.isSelfSignedCertificateAllowed(),
|
|
164
186
|
};
|
|
165
187
|
}
|
|
166
188
|
|
|
167
189
|
toString() {
|
|
168
|
-
return `${this.getHost()}:${this.getHttpPort()}:${this.getGrpcPort()}`;
|
|
190
|
+
return `${this.getProtocol()}://${this.getHost()}:${this.getHttpPort()}:${this.getGrpcPort()}`;
|
|
169
191
|
}
|
|
170
192
|
}
|
|
171
193
|
|
|
172
194
|
DAPIAddress.DEFAULT_HTTP_PORT = 3000;
|
|
173
195
|
DAPIAddress.DEFAULT_GRPC_PORT = 3010;
|
|
196
|
+
DAPIAddress.DEFAULT_PROTOCOL = 'http';
|
|
174
197
|
|
|
175
198
|
/**
|
|
176
199
|
* @typedef {object} RawDAPIAddress
|
|
200
|
+
* @property {string} protocol
|
|
177
201
|
* @property {string} host
|
|
178
202
|
* @property {number} [httpPort=3000]
|
|
179
203
|
* @property {number} [grpcPort=3010]
|
|
180
204
|
* @property {string} [proRegTxHash]
|
|
205
|
+
* @property {bool} [selfSigned]
|
|
181
206
|
*/
|
|
182
207
|
|
|
183
208
|
module.exports = DAPIAddress;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const sample = require('lodash
|
|
1
|
+
const sample = require('lodash/sample');
|
|
2
2
|
const networks = require('@dashevo/dashcore-lib/lib/networks');
|
|
3
3
|
|
|
4
4
|
class ListDAPIAddressProvider {
|
|
@@ -36,7 +36,9 @@ class ListDAPIAddressProvider {
|
|
|
36
36
|
if (network && network.regtestEnabled) {
|
|
37
37
|
const randomNodeIndex = Math.floor(Math.random() * liveAddresses.length);
|
|
38
38
|
|
|
39
|
+
liveAddress.protocol = 'https';
|
|
39
40
|
liveAddress.host = '127.0.0.1';
|
|
41
|
+
liveAddress.allowSelfSignedCertificate = true;
|
|
40
42
|
liveAddress.httpPort = 3000 + randomNodeIndex * 100;
|
|
41
43
|
liveAddress.grpcPort = 3010 + randomNodeIndex * 100;
|
|
42
44
|
}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
class Metadata {
|
|
2
2
|
/**
|
|
3
|
-
* @param {
|
|
3
|
+
* @param {Object} properties
|
|
4
4
|
* @param {number} properties.height - block height
|
|
5
5
|
* @param {number} properties.coreChainLockedHeight - core chain locked height
|
|
6
|
+
* @param {number} properties.timeMs - block time
|
|
7
|
+
* @param {number} properties.protocolVersion - protocol version
|
|
6
8
|
*/
|
|
7
9
|
constructor(properties) {
|
|
8
10
|
this.height = properties.height;
|
|
9
11
|
this.coreChainLockedHeight = properties.coreChainLockedHeight;
|
|
12
|
+
this.timeMs = properties.timeMs;
|
|
13
|
+
this.protocolVersion = properties.protocolVersion;
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
/**
|
|
@@ -26,6 +30,22 @@ class Metadata {
|
|
|
26
30
|
getCoreChainLockedHeight() {
|
|
27
31
|
return this.coreChainLockedHeight;
|
|
28
32
|
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get block time
|
|
36
|
+
* @return {number}
|
|
37
|
+
*/
|
|
38
|
+
getTimeMs() {
|
|
39
|
+
return this.timeMs;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get protocol version
|
|
44
|
+
* @return {number}
|
|
45
|
+
*/
|
|
46
|
+
getProtocolVersion() {
|
|
47
|
+
return this.protocolVersion;
|
|
48
|
+
}
|
|
29
49
|
}
|
|
30
50
|
|
|
31
51
|
module.exports = Metadata;
|
|
@@ -2,13 +2,15 @@ class Proof {
|
|
|
2
2
|
/**
|
|
3
3
|
* @param {object} properties
|
|
4
4
|
* @param {Buffer} properties.merkleProof
|
|
5
|
-
* @param {Buffer} properties.
|
|
5
|
+
* @param {Buffer} properties.quorumHash
|
|
6
6
|
* @param {Buffer} properties.signature
|
|
7
|
+
* @param {number} properties.round
|
|
7
8
|
*/
|
|
8
9
|
constructor(properties) {
|
|
9
10
|
this.merkleProof = properties.merkleProof;
|
|
10
|
-
this.
|
|
11
|
+
this.quorumHash = properties.quorumHash;
|
|
11
12
|
this.signature = properties.signature;
|
|
13
|
+
this.round = properties.round;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
/**
|
|
@@ -21,8 +23,8 @@ class Proof {
|
|
|
21
23
|
/**
|
|
22
24
|
* @returns {Buffer}
|
|
23
25
|
*/
|
|
24
|
-
|
|
25
|
-
return this.
|
|
26
|
+
getQuorumHash() {
|
|
27
|
+
return this.quorumHash;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
/**
|
|
@@ -32,6 +34,14 @@ class Proof {
|
|
|
32
34
|
return this.signature;
|
|
33
35
|
}
|
|
34
36
|
|
|
37
|
+
/**
|
|
38
|
+
*
|
|
39
|
+
* @returns {number}
|
|
40
|
+
*/
|
|
41
|
+
getRound() {
|
|
42
|
+
return this.round;
|
|
43
|
+
}
|
|
44
|
+
|
|
35
45
|
/**
|
|
36
46
|
* @param {Object} proofProto
|
|
37
47
|
*
|
|
@@ -40,8 +50,9 @@ class Proof {
|
|
|
40
50
|
static createFromProto(proofProto) {
|
|
41
51
|
return new Proof({
|
|
42
52
|
merkleProof: Buffer.from(proofProto.getMerkleProof()),
|
|
43
|
-
|
|
53
|
+
quorumHash: Buffer.from(proofProto.getQuorumHash()),
|
|
44
54
|
signature: Buffer.from(proofProto.getSignature()),
|
|
55
|
+
round: proofProto.getRound(),
|
|
45
56
|
});
|
|
46
57
|
}
|
|
47
58
|
}
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @returns {{
|
|
2
|
+
* @returns {{
|
|
3
|
+
* coreChainLockedHeight: number,
|
|
4
|
+
* height: number,
|
|
5
|
+
* signature: Buffer,
|
|
6
|
+
* protocolVersion: number,
|
|
7
|
+
* timeMs: number,
|
|
8
|
+
* }}
|
|
3
9
|
*/
|
|
4
10
|
function getMetadataFixture() {
|
|
5
11
|
return {
|
|
6
12
|
height: 10,
|
|
7
13
|
coreChainLockedHeight: 42,
|
|
14
|
+
timeMs: new Date().getTime(),
|
|
15
|
+
protocolVersion: 1,
|
|
8
16
|
};
|
|
9
17
|
}
|
|
10
18
|
|
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
* @returns {{
|
|
3
3
|
* merkleProof: Buffer,
|
|
4
4
|
* signature: Buffer,
|
|
5
|
-
*
|
|
5
|
+
* quorumHash: Buffer
|
|
6
6
|
* }}
|
|
7
7
|
*/
|
|
8
8
|
function getProofFixture() {
|
|
9
9
|
return {
|
|
10
|
-
|
|
10
|
+
quorumHash: Buffer.from('AQEBAQEBAQEBAQEB', 'base64'),
|
|
11
11
|
signature: Buffer.from('AgICAgICAgICAgIC', 'base64'),
|
|
12
12
|
merkleProof: Buffer.from('0100000001f0faf5f55674905a68eba1be2f946e667c1cb5010101', 'hex'),
|
|
13
|
+
round: 42,
|
|
13
14
|
};
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -7,20 +7,12 @@ class BlockHeadersWithChainLocksStreamMock extends EventEmitter {
|
|
|
7
7
|
sinon.spy(this, 'on');
|
|
8
8
|
sinon.spy(this, 'removeListener');
|
|
9
9
|
sinon.spy(this, 'emit');
|
|
10
|
-
sinon.spy(this, 'destroy');
|
|
11
10
|
sinon.spy(this, 'removeAllListeners');
|
|
12
11
|
sinon.spy(this, 'cancel');
|
|
13
12
|
|
|
14
13
|
this.errored = false;
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
destroy(e) {
|
|
18
|
-
this.emit('end');
|
|
19
|
-
if (e) {
|
|
20
|
-
this.emit('error', e);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
16
|
cancel() {
|
|
25
17
|
if (!this.errored) {
|
|
26
18
|
const err = new Error('CANCELED_ON_CLIENT');
|
|
@@ -48,6 +40,15 @@ class BlockHeadersWithChainLocksStreamMock extends EventEmitter {
|
|
|
48
40
|
}),
|
|
49
41
|
});
|
|
50
42
|
}
|
|
43
|
+
|
|
44
|
+
errorHandler(e) {
|
|
45
|
+
this.emit('error', e);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
end() {
|
|
49
|
+
this.emit('end');
|
|
50
|
+
this.removeAllListeners();
|
|
51
|
+
}
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
module.exports = BlockHeadersWithChainLocksStreamMock;
|
|
@@ -132,6 +132,7 @@ class GrpcTransport {
|
|
|
132
132
|
* @returns {string}
|
|
133
133
|
*/
|
|
134
134
|
makeGrpcUrlFromAddress(address) {
|
|
135
|
+
let protocol = address.getProtocol();
|
|
135
136
|
let port = address.getHttpPort();
|
|
136
137
|
|
|
137
138
|
// For NodeJS Client
|
|
@@ -139,9 +140,11 @@ class GrpcTransport {
|
|
|
139
140
|
&& process.versions != null
|
|
140
141
|
&& process.versions.node != null) {
|
|
141
142
|
port = address.getGrpcPort();
|
|
142
|
-
}
|
|
143
143
|
|
|
144
|
-
|
|
144
|
+
if (address.isSelfSignedCertificateAllowed()) {
|
|
145
|
+
protocol = 'http';
|
|
146
|
+
}
|
|
147
|
+
}
|
|
145
148
|
|
|
146
149
|
return `${protocol}://${address.getHost()}:${port}`;
|
|
147
150
|
}
|
|
@@ -2,7 +2,7 @@ const cbor = require('cbor');
|
|
|
2
2
|
|
|
3
3
|
const createConsensusError = require('@dashevo/dpp/lib/errors/consensus/createConsensusError');
|
|
4
4
|
const GrpcErrorCodes = require('@dashevo/grpc-common/lib/server/error/GrpcErrorCodes');
|
|
5
|
-
const {
|
|
5
|
+
const { parseMetadata } = require('@dashevo/dapi-grpc');
|
|
6
6
|
|
|
7
7
|
const NotFoundError = require('./errors/NotFoundError');
|
|
8
8
|
const TimeoutError = require('./errors/TimeoutError');
|
|
@@ -50,15 +50,7 @@ function createGrpcTransportError(grpcError, dapiAddress) {
|
|
|
50
50
|
|
|
51
51
|
const message = grpcError.details || grpcError.message;
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (grpcError.metadata) {
|
|
56
|
-
// In cases of gRPC-Web client we get plain map instead of Metadata instance
|
|
57
|
-
metadata = grpcError.metadata;
|
|
58
|
-
if (grpcError.metadata instanceof Metadata) {
|
|
59
|
-
metadata = grpcError.metadata.getMap();
|
|
60
|
-
}
|
|
61
|
-
}
|
|
53
|
+
const metadata = parseMetadata(grpcError.metadata) || {};
|
|
62
54
|
|
|
63
55
|
// Error data
|
|
64
56
|
const driveErrorData = metadata['drive-error-data-bin'];
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
const axios = require('axios');
|
|
2
|
+
const https = require('https');
|
|
2
3
|
|
|
3
4
|
const JsonRpcError = require('./errors/JsonRpcError');
|
|
4
5
|
const WrongHttpCodeError = require('./errors/WrongHttpCodeError');
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* @typedef {requestJsonRpc}
|
|
9
|
+
* @param {string} protocol
|
|
8
10
|
* @param {string} host
|
|
9
11
|
* @param {number} port
|
|
12
|
+
* @param {boolean} selfSigned
|
|
10
13
|
* @param {string} method
|
|
11
14
|
* @param {object} params
|
|
12
15
|
* @param {object} [options]
|
|
13
16
|
* @returns {Promise<*>}
|
|
14
17
|
*/
|
|
15
|
-
async function requestJsonRpc(host, port, method, params, options = {}) {
|
|
16
|
-
const protocol = port === 443 ? 'https' : 'http';
|
|
17
|
-
|
|
18
|
+
async function requestJsonRpc(protocol, host, port, selfSigned, method, params, options = {}) {
|
|
18
19
|
const url = `${protocol}://${host}${port && port !== 443 ? `:${port}` : ''}`;
|
|
19
20
|
|
|
20
21
|
const payload = {
|
|
@@ -30,8 +31,10 @@ async function requestJsonRpc(host, port, method, params, options = {}) {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
const requestInfo = {
|
|
34
|
+
protocol,
|
|
33
35
|
host,
|
|
34
36
|
port,
|
|
37
|
+
selfSigned,
|
|
35
38
|
method,
|
|
36
39
|
params,
|
|
37
40
|
options,
|
|
@@ -39,11 +42,23 @@ async function requestJsonRpc(host, port, method, params, options = {}) {
|
|
|
39
42
|
|
|
40
43
|
let response;
|
|
41
44
|
|
|
45
|
+
const config = { timeout: options.timeout };
|
|
46
|
+
// For NodeJS Client
|
|
47
|
+
if (typeof process !== 'undefined'
|
|
48
|
+
&& process.versions != null
|
|
49
|
+
&& process.versions.node != null
|
|
50
|
+
&& protocol === 'https'
|
|
51
|
+
&& selfSigned) {
|
|
52
|
+
config.httpsAgent = new https.Agent({
|
|
53
|
+
rejectUnauthorized: false,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
42
57
|
try {
|
|
43
58
|
response = await axios.post(
|
|
44
59
|
url,
|
|
45
60
|
payload,
|
|
46
|
-
|
|
61
|
+
config,
|
|
47
62
|
);
|
|
48
63
|
} catch (error) {
|
|
49
64
|
if (error.response && error.response.status >= 500) {
|
|
@@ -2,9 +2,18 @@ const EventEmitter = require('events');
|
|
|
2
2
|
const GrpcErrorCodes = require('@dashevo/grpc-common/lib/server/error/GrpcErrorCodes');
|
|
3
3
|
const wait = require('../utils/wait');
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @typedef ReconnectableStreamOptions
|
|
7
|
+
* @property {number} [autoReconnectInterval]
|
|
8
|
+
* interval in MS to perform auto reconnect
|
|
9
|
+
* @property {number} [maxRetriesOnError]
|
|
10
|
+
* maximum amount of retry attempts on error happens. If set to -1, retries are infinite
|
|
11
|
+
* @property {number} [retryOnErrorDelay]
|
|
12
|
+
* delay in MS to perform retry after an error
|
|
13
|
+
*/
|
|
5
14
|
const defaultOptions = {
|
|
6
15
|
// TODO: manage timeout according to the Nginx setting of the node
|
|
7
|
-
|
|
16
|
+
autoReconnectInterval: 50000,
|
|
8
17
|
maxRetriesOnError: 10,
|
|
9
18
|
retryOnErrorDelay: 1000,
|
|
10
19
|
};
|
|
@@ -17,69 +26,121 @@ const EVENTS = {
|
|
|
17
26
|
};
|
|
18
27
|
|
|
19
28
|
/**
|
|
20
|
-
*
|
|
29
|
+
* A wrapper around grpc-js/grpc-web streams that provides auto-reconnect
|
|
30
|
+
* and retry on error functionality
|
|
31
|
+
* - Auto reconnect is needed to not hang on one node for too long.
|
|
32
|
+
* It happens within provided interval
|
|
33
|
+
* - Retry on error logic performs retry attempts until it reaches it's limit.
|
|
34
|
+
* After reaching limit, ReconnectableStream emits an error.
|
|
21
35
|
*/
|
|
22
36
|
class ReconnectableStream extends EventEmitter {
|
|
23
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Helper that wraps stream creation function and performs auto connection
|
|
39
|
+
*
|
|
40
|
+
* @param {Function} createStreamFunction - function returning grpc-js/grpc-web stream
|
|
41
|
+
* @param {ReconnectableStreamOptions} options
|
|
42
|
+
* @returns {function(...[*]): Promise<ReconnectableStream>}
|
|
43
|
+
*/
|
|
44
|
+
static create(createStreamFunction, options) {
|
|
24
45
|
return async (...args) => {
|
|
25
|
-
const reconnectableStream = new ReconnectableStream(
|
|
46
|
+
const reconnectableStream = new ReconnectableStream(createStreamFunction, options);
|
|
26
47
|
await reconnectableStream.connect(...args);
|
|
27
48
|
return reconnectableStream;
|
|
28
49
|
};
|
|
29
50
|
}
|
|
30
51
|
|
|
31
|
-
|
|
52
|
+
/**
|
|
53
|
+
* @param {Function} createStream - function returning grpc-js/grpc-web stream
|
|
54
|
+
* @param {ReconnectableStreamOptions} options
|
|
55
|
+
*/
|
|
56
|
+
constructor(createStream, options = {}) {
|
|
32
57
|
super();
|
|
33
|
-
this.stream = null;
|
|
34
|
-
this.streamFunction = streamFunction;
|
|
35
|
-
this.reconnectTimeout = null;
|
|
36
58
|
|
|
37
59
|
const opts = { ...defaultOptions, ...options };
|
|
38
|
-
|
|
39
|
-
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Auto-reconnect interval in millisecond
|
|
63
|
+
* It is needed to automatically reconnect to another DAPI node
|
|
64
|
+
*
|
|
65
|
+
* @type {number}
|
|
66
|
+
*/
|
|
67
|
+
this.autoReconnectInterval = opts.autoReconnectInterval;
|
|
40
68
|
this.retryOnErrorDelay = opts.retryOnErrorDelay;
|
|
41
|
-
// Assign setTimeout and clearTimeout as class method for tests
|
|
42
|
-
this.setTimeout = (callback, delay) => setTimeout(callback, delay);
|
|
43
69
|
|
|
44
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Max amount of retries on error
|
|
72
|
+
*/
|
|
73
|
+
this.maxRetriesOnError = opts.maxRetriesOnError;
|
|
45
74
|
|
|
46
75
|
/**
|
|
47
|
-
*
|
|
76
|
+
* Current amount of retries on error
|
|
77
|
+
* (Does not have effect if maxRetriesOnError === -1)
|
|
78
|
+
*/
|
|
79
|
+
this.retriesOnError = 0;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* createStream arguments
|
|
48
83
|
*/
|
|
49
84
|
this.args = null;
|
|
85
|
+
/**
|
|
86
|
+
* grpc stream
|
|
87
|
+
*/
|
|
88
|
+
this.stream = null;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Function to wrap around to handle interval reconnects and error retries
|
|
92
|
+
*/
|
|
93
|
+
this.createStream = createStream;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Reference to setTimeout managed by autoReconnectInterval
|
|
97
|
+
*/
|
|
98
|
+
this.reconnectTimeout = null;
|
|
99
|
+
|
|
100
|
+
// For mocks
|
|
101
|
+
this.setTimeout = (callback, delay) => setTimeout(callback, delay);
|
|
102
|
+
this.clearTimeout = (reference) => clearTimeout(reference);
|
|
50
103
|
|
|
51
104
|
this.connect = this.connect.bind(this);
|
|
52
105
|
this.reconnect = this.reconnect.bind(this);
|
|
53
106
|
this.cancel = this.cancel.bind(this);
|
|
54
|
-
this.destroy = this.destroy.bind(this);
|
|
55
107
|
this.errorHandler = this.errorHandler.bind(this);
|
|
56
108
|
this.dataHandler = this.dataHandler.bind(this);
|
|
57
109
|
this.endHandler = this.endHandler.bind(this);
|
|
58
110
|
this.addListeners = this.addListeners.bind(this);
|
|
59
|
-
|
|
60
|
-
// Manages retry attempts when error happens
|
|
61
|
-
this.retriesOnError = 0;
|
|
62
111
|
}
|
|
63
112
|
|
|
64
113
|
async connect(...args) {
|
|
114
|
+
// Memorize current stream args (which can be altered by beforeReconnect logic)
|
|
65
115
|
this.args = args;
|
|
66
|
-
|
|
116
|
+
|
|
117
|
+
// Create grpc stream
|
|
118
|
+
this.stream = await this.createStream(...this.args);
|
|
119
|
+
|
|
120
|
+
// Add event listeners
|
|
67
121
|
this.addListeners();
|
|
68
122
|
|
|
69
|
-
if (this.
|
|
70
|
-
this.
|
|
71
|
-
this.reconnect,
|
|
72
|
-
this.reconnectTimeoutDelay,
|
|
73
|
-
);
|
|
123
|
+
if (this.autoReconnectInterval > 0) {
|
|
124
|
+
this.startAutoReconnect();
|
|
74
125
|
}
|
|
75
126
|
}
|
|
76
127
|
|
|
128
|
+
startAutoReconnect() {
|
|
129
|
+
if (this.reconnectTimeout) {
|
|
130
|
+
throw new Error('Auto reconnect timeout is already running.');
|
|
131
|
+
}
|
|
132
|
+
this.reconnectTimeout = this.setTimeout(
|
|
133
|
+
this.reconnect,
|
|
134
|
+
this.autoReconnectInterval,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
77
138
|
reconnect() {
|
|
78
139
|
if (this.reconnectTimeout) {
|
|
79
140
|
this.reconnectTimeout = null;
|
|
80
141
|
this.stream.cancel();
|
|
81
142
|
|
|
82
|
-
let newArgs = this.args;
|
|
143
|
+
let newArgs = this.args || [];
|
|
83
144
|
const updateArgs = (...args) => {
|
|
84
145
|
newArgs = args;
|
|
85
146
|
};
|
|
@@ -91,6 +152,10 @@ class ReconnectableStream extends EventEmitter {
|
|
|
91
152
|
}
|
|
92
153
|
|
|
93
154
|
/**
|
|
155
|
+
* Function that adds EventEmitter style listeners
|
|
156
|
+
* to the stream and also rewires stream cancel function
|
|
157
|
+
* in order to automatically unsubscribe from events
|
|
158
|
+
*
|
|
94
159
|
* @private
|
|
95
160
|
*/
|
|
96
161
|
addListeners() {
|
|
@@ -100,6 +165,10 @@ class ReconnectableStream extends EventEmitter {
|
|
|
100
165
|
|
|
101
166
|
const { cancel } = this.stream;
|
|
102
167
|
|
|
168
|
+
// Rewire cancel function in order to
|
|
169
|
+
// unsubscribe from DATA and END events.
|
|
170
|
+
// We don't unsubscribe from ERROR event because it
|
|
171
|
+
// handles `CANCELLED_ON_CLIENT` error after calling .cancel() and must stay
|
|
103
172
|
this.stream.cancel = () => {
|
|
104
173
|
this.stream.removeListener(EVENTS.DATA, this.dataHandler);
|
|
105
174
|
this.stream.removeListener(EVENTS.END, this.endHandler);
|
|
@@ -107,72 +176,107 @@ class ReconnectableStream extends EventEmitter {
|
|
|
107
176
|
};
|
|
108
177
|
}
|
|
109
178
|
|
|
179
|
+
/**
|
|
180
|
+
* stream.on('data') handler
|
|
181
|
+
*
|
|
182
|
+
* @param data
|
|
183
|
+
*/
|
|
110
184
|
dataHandler(data) {
|
|
111
185
|
this.emit(EVENTS.DATA, data);
|
|
112
186
|
}
|
|
113
187
|
|
|
114
188
|
/**
|
|
189
|
+
* stream.on('end') handler
|
|
190
|
+
*
|
|
115
191
|
* @private
|
|
116
192
|
*/
|
|
117
193
|
endHandler() {
|
|
118
|
-
this.
|
|
194
|
+
this.stopAutoReconnect();
|
|
119
195
|
this.stream = null;
|
|
120
196
|
this.emit(EVENTS.END);
|
|
121
197
|
}
|
|
122
198
|
|
|
123
199
|
/**
|
|
200
|
+
* stream.on('error') handler
|
|
201
|
+
*
|
|
124
202
|
* @private
|
|
125
203
|
* @param e
|
|
126
204
|
*/
|
|
127
205
|
errorHandler(e) {
|
|
128
|
-
|
|
206
|
+
// In case of cancellation nothing has to happen.
|
|
207
|
+
// TODO: do we need to propagate GrpcErrorCodes.CANCELLED further?
|
|
129
208
|
if (e.code === GrpcErrorCodes.CANCELLED) {
|
|
130
209
|
return;
|
|
131
210
|
}
|
|
132
|
-
this.stopReconnectTimeout();
|
|
133
211
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
212
|
+
// Retry stream
|
|
213
|
+
this.retryOnError(e);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Manages retry logic
|
|
218
|
+
*
|
|
219
|
+
* @param e
|
|
220
|
+
*/
|
|
221
|
+
retryOnError(e) {
|
|
222
|
+
// Stop reconnect timeout if there is one
|
|
223
|
+
this.stopAutoReconnect();
|
|
224
|
+
|
|
225
|
+
// Unsubscribe from events in case of the external call
|
|
226
|
+
this.stream.removeListener(EVENTS.END, this.endHandler);
|
|
227
|
+
this.stream.removeListener(EVENTS.DATA, this.dataHandler);
|
|
228
|
+
|
|
229
|
+
const canRetry = this.maxRetriesOnError === -1 // Infinite retries
|
|
230
|
+
|| this.retriesOnError < this.maxRetriesOnError; // Or less than max limit
|
|
231
|
+
|
|
232
|
+
if (canRetry) {
|
|
233
|
+
// Handle 'beforeReconnect` logic
|
|
234
|
+
let newArgs = this.args || [];
|
|
235
|
+
const updateArgsBeforeReconnect = (...args) => {
|
|
140
236
|
newArgs = args;
|
|
141
237
|
};
|
|
142
238
|
|
|
143
|
-
|
|
239
|
+
// Emit beforeReconnect event in case parent would want to alter arguments
|
|
240
|
+
this.emit(EVENTS.BEFORE_RECONNECT, updateArgsBeforeReconnect);
|
|
144
241
|
|
|
242
|
+
// Wait before reconnecting
|
|
145
243
|
wait(this.retryOnErrorDelay)
|
|
146
|
-
.then(() => this.connect(...newArgs))
|
|
244
|
+
.then(() => this.connect(...newArgs))// Reconnect with new args
|
|
147
245
|
.then(() => {
|
|
148
|
-
this.retriesOnError += 1;
|
|
246
|
+
this.retriesOnError += 1; // Increase amount of current retries
|
|
149
247
|
})
|
|
150
248
|
.catch((connectError) => {
|
|
151
|
-
this.emit(EVENTS.ERROR, connectError);
|
|
249
|
+
this.emit(EVENTS.ERROR, connectError); // Or emit error in case retry has failed
|
|
152
250
|
});
|
|
153
251
|
} else {
|
|
252
|
+
// Or simply emit error in case retry attempts were exhausted
|
|
154
253
|
this.emit(EVENTS.ERROR, e);
|
|
155
254
|
}
|
|
156
255
|
}
|
|
157
256
|
|
|
158
257
|
/**
|
|
258
|
+
* Stops auto reconnect timeout
|
|
259
|
+
*
|
|
159
260
|
* @private
|
|
160
261
|
*/
|
|
161
|
-
|
|
262
|
+
stopAutoReconnect() {
|
|
162
263
|
if (this.reconnectTimeout) {
|
|
163
264
|
this.clearTimeout(this.reconnectTimeout);
|
|
164
265
|
this.reconnectTimeout = null;
|
|
165
266
|
}
|
|
166
267
|
}
|
|
167
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Cancels stream.
|
|
271
|
+
* Returns `stream.cancel()` to handle it from parent if needed.
|
|
272
|
+
* (grpc-js cancel() is asynchronous and grpc-web is synchronous)
|
|
273
|
+
*
|
|
274
|
+
* @returns {*}
|
|
275
|
+
*/
|
|
168
276
|
cancel() {
|
|
169
|
-
this.
|
|
277
|
+
this.stopAutoReconnect();
|
|
170
278
|
return this.stream.cancel();
|
|
171
279
|
}
|
|
172
|
-
|
|
173
|
-
destroy(e) {
|
|
174
|
-
this.stream.destroy(e);
|
|
175
|
-
}
|
|
176
280
|
}
|
|
177
281
|
|
|
178
282
|
ReconnectableStream.EVENTS = EVENTS;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dashevo/dapi-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0-dev.10",
|
|
4
4
|
"description": "Client library used to access Dash DAPI endpoints",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"contributors": [
|
|
@@ -26,16 +26,15 @@
|
|
|
26
26
|
}
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@dashevo/dapi-grpc": "0.
|
|
30
|
-
"@dashevo/dash-spv": "0.
|
|
29
|
+
"@dashevo/dapi-grpc": "0.24.0-dev.10",
|
|
30
|
+
"@dashevo/dash-spv": "0.24.0-dev.10",
|
|
31
31
|
"@dashevo/dashcore-lib": "~0.19.44",
|
|
32
|
-
"@dashevo/dpp": "0.
|
|
33
|
-
"@dashevo/grpc-common": "0.
|
|
34
|
-
"@grpc/grpc-js": "^1.3.7",
|
|
32
|
+
"@dashevo/dpp": "0.24.0-dev.10",
|
|
33
|
+
"@dashevo/grpc-common": "0.24.0-dev.10",
|
|
35
34
|
"axios": "^0.27.2",
|
|
36
35
|
"bs58": "^4.0.1",
|
|
37
36
|
"cbor": "^8.0.0",
|
|
38
|
-
"lodash
|
|
37
|
+
"lodash": "^4.17.21",
|
|
39
38
|
"node-inspect-extracted": "^1.0.8",
|
|
40
39
|
"wasm-x11-hash": "~0.0.2"
|
|
41
40
|
},
|
|
@@ -55,6 +54,7 @@
|
|
|
55
54
|
"eslint-plugin-import": "^2.24.2",
|
|
56
55
|
"eslint-plugin-jsdoc": "^27.0.0",
|
|
57
56
|
"events": "^3.3.0",
|
|
57
|
+
"google-protobuf": "^3.12.2",
|
|
58
58
|
"karma": "^6.4.1",
|
|
59
59
|
"karma-chai": "^0.1.0",
|
|
60
60
|
"karma-chrome-launcher": "^3.1.0",
|