@electrum-cash/network 4.1.4-development.11427964670 → 4.1.4-development.12703457447
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/dist/index.d.mts +438 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +678 -867
- package/dist/index.mjs.map +1 -1
- package/package.json +13 -18
- package/dist/index.d.ts +0 -413
- package/dist/index.d.ts.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,881 +1,692 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import debug from "@electrum-cash/debug-logs";
|
|
2
|
+
import { ElectrumWebSocket } from "@electrum-cash/web-socket";
|
|
3
|
+
import { EventEmitter } from "eventemitter3";
|
|
4
|
+
import { parse, parseNumberAndBigInt } from "lossless-json";
|
|
5
|
+
import { Mutex } from "async-mutex";
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return '\n';
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
var $e83d2e7688025acd$exports = {};
|
|
52
|
-
|
|
53
|
-
$parcel$export($e83d2e7688025acd$exports, "isVersionRejected", () => $e83d2e7688025acd$export$e1f38ab2b4ebdde6);
|
|
54
|
-
$parcel$export($e83d2e7688025acd$exports, "isVersionNegotiated", () => $e83d2e7688025acd$export$9598f0c76aa41d73);
|
|
55
|
-
const $e83d2e7688025acd$export$e1f38ab2b4ebdde6 = function(object) {
|
|
56
|
-
return 'error' in object;
|
|
57
|
-
};
|
|
58
|
-
const $e83d2e7688025acd$export$9598f0c76aa41d73 = function(object) {
|
|
59
|
-
return 'software' in object && 'protocol' in object;
|
|
7
|
+
//#region source/electrum-protocol.ts
|
|
8
|
+
/**
|
|
9
|
+
* Grouping of utilities that simplifies implementation of the Electrum protocol.
|
|
10
|
+
*
|
|
11
|
+
* @ignore
|
|
12
|
+
*/
|
|
13
|
+
var ElectrumProtocol = class {
|
|
14
|
+
/**
|
|
15
|
+
* Helper function that builds an Electrum request object.
|
|
16
|
+
*
|
|
17
|
+
* @param method - method to call.
|
|
18
|
+
* @param parameters - method parameters for the call.
|
|
19
|
+
* @param requestId - unique string or number referencing this request.
|
|
20
|
+
*
|
|
21
|
+
* @returns a properly formatted Electrum request string.
|
|
22
|
+
*/
|
|
23
|
+
static buildRequestObject(method, parameters, requestId) {
|
|
24
|
+
return JSON.stringify({
|
|
25
|
+
method,
|
|
26
|
+
params: parameters,
|
|
27
|
+
id: requestId
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Constant used to verify if a provided string is a valid version number.
|
|
32
|
+
*
|
|
33
|
+
* @returns a regular expression that matches valid version numbers.
|
|
34
|
+
*/
|
|
35
|
+
static get versionRegexp() {
|
|
36
|
+
return /^\d+(\.\d+)+$/;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Constant used to separate statements/messages in a stream of data.
|
|
40
|
+
*
|
|
41
|
+
* @returns the delimiter used by Electrum to separate statements.
|
|
42
|
+
*/
|
|
43
|
+
static get statementDelimiter() {
|
|
44
|
+
return "\n";
|
|
45
|
+
}
|
|
60
46
|
};
|
|
61
47
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region source/rpc-interfaces.ts
|
|
50
|
+
const isRPCErrorResponse = function(message) {
|
|
51
|
+
return "id" in message && "error" in message;
|
|
66
52
|
};
|
|
67
|
-
const
|
|
68
|
-
|
|
53
|
+
const isRPCNotification = function(message) {
|
|
54
|
+
return !("id" in message) && "method" in message;
|
|
69
55
|
};
|
|
70
|
-
const $abcb763a48577a1e$export$280de919a0cf6928 = function(message) {
|
|
71
|
-
return !('id' in message) && 'method' in message;
|
|
72
|
-
};
|
|
73
|
-
const $abcb763a48577a1e$export$94e3360fcddccc76 = function(message) {
|
|
74
|
-
return 'id' in message && 'method' in message;
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
var $db7c797e63383364$exports = {};
|
|
80
56
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region source/enums.ts
|
|
59
|
+
/**
|
|
60
|
+
* Enum that denotes the connection status of an ElectrumConnection.
|
|
61
|
+
* @enum {number}
|
|
62
|
+
* @property {0} DISCONNECTED The connection is disconnected.
|
|
63
|
+
* @property {1} AVAILABLE The connection is connected.
|
|
64
|
+
* @property {2} DISCONNECTING The connection is disconnecting.
|
|
65
|
+
* @property {3} CONNECTING The connection is connecting.
|
|
66
|
+
* @property {4} RECONNECTING The connection is restarting.
|
|
67
|
+
*/
|
|
68
|
+
let ConnectionStatus = /* @__PURE__ */ function(ConnectionStatus$1) {
|
|
69
|
+
ConnectionStatus$1[ConnectionStatus$1["DISCONNECTED"] = 0] = "DISCONNECTED";
|
|
70
|
+
ConnectionStatus$1[ConnectionStatus$1["CONNECTED"] = 1] = "CONNECTED";
|
|
71
|
+
ConnectionStatus$1[ConnectionStatus$1["DISCONNECTING"] = 2] = "DISCONNECTING";
|
|
72
|
+
ConnectionStatus$1[ConnectionStatus$1["CONNECTING"] = 3] = "CONNECTING";
|
|
73
|
+
ConnectionStatus$1[ConnectionStatus$1["RECONNECTING"] = 4] = "RECONNECTING";
|
|
74
|
+
return ConnectionStatus$1;
|
|
98
75
|
}({});
|
|
99
76
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
* @param version - protocol version to use with the host.
|
|
108
|
-
* @param socketOrHostname - pre-configured electrum socket or fully qualified domain name or IP number of the host
|
|
109
|
-
* @param options - ...
|
|
110
|
-
*
|
|
111
|
-
* @throws {Error} if `version` is not a valid version string.
|
|
112
|
-
*/ constructor(application, version, socketOrHostname, options){
|
|
113
|
-
// Initialize the event emitter.
|
|
114
|
-
super(), this.application = application, this.version = version, this.socketOrHostname = socketOrHostname, this.options = options, this.status = (0, $db7c797e63383364$export$7516420eb880ab68).DISCONNECTED, this.verifications = [], this.messageBuffer = '';
|
|
115
|
-
// Check if the provided version is a valid version number.
|
|
116
|
-
if (!(0, $24139611f53a54b8$export$5d955335434540c6).versionRegexp.test(version)) // Throw an error since the version number was not valid.
|
|
117
|
-
throw new Error(`Provided version string (${version}) is not a valid protocol version number.`);
|
|
118
|
-
// If a hostname was provided..
|
|
119
|
-
if (typeof socketOrHostname === 'string') // Use a web socket with default parameters.
|
|
120
|
-
this.socket = new (0, $dvphU$ElectrumWebSocket)(socketOrHostname);
|
|
121
|
-
else // Use the provided socket.
|
|
122
|
-
this.socket = socketOrHostname;
|
|
123
|
-
// Set up handlers for connection and disconnection.
|
|
124
|
-
this.socket.on('connected', this.onSocketConnect.bind(this));
|
|
125
|
-
this.socket.on('disconnected', this.onSocketDisconnect.bind(this));
|
|
126
|
-
// Set up handler for incoming data.
|
|
127
|
-
this.socket.on('data', this.parseMessageChunk.bind(this));
|
|
128
|
-
// Handle visibility changes when run in a browser environment (if not explicitly disabled).
|
|
129
|
-
if (typeof document !== 'undefined' && !this.options.disableBrowserVisibilityHandling) document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
|
|
130
|
-
// Handle network connection changes when run in a browser environment (if not explicitly disabled).
|
|
131
|
-
if (typeof window !== 'undefined' && !this.options.disableBrowserConnectivityHandling) {
|
|
132
|
-
window.addEventListener('online', this.handleNetworkChange.bind(this));
|
|
133
|
-
window.addEventListener('offline', this.handleNetworkChange.bind(this));
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
// Expose hostIdentifier from the socket.
|
|
137
|
-
get hostIdentifier() {
|
|
138
|
-
return this.socket.hostIdentifier;
|
|
139
|
-
}
|
|
140
|
-
// Expose port from the socket.
|
|
141
|
-
get encrypted() {
|
|
142
|
-
return this.socket.encrypted;
|
|
143
|
-
}
|
|
144
|
-
/**
|
|
145
|
-
* Assembles incoming data into statements and hands them off to the message parser.
|
|
146
|
-
*
|
|
147
|
-
* @param data - data to append to the current message buffer, as a string.
|
|
148
|
-
*
|
|
149
|
-
* @throws {SyntaxError} if the passed statement parts are not valid JSON.
|
|
150
|
-
*/ parseMessageChunk(data) {
|
|
151
|
-
// Update the timestamp for when we last received data.
|
|
152
|
-
this.lastReceivedTimestamp = Date.now();
|
|
153
|
-
// Emit a notification indicating that the connection has received data.
|
|
154
|
-
this.emit('received');
|
|
155
|
-
// Clear and remove all verification timers.
|
|
156
|
-
this.verifications.forEach((timer)=>clearTimeout(timer));
|
|
157
|
-
this.verifications.length = 0;
|
|
158
|
-
// Add the message to the current message buffer.
|
|
159
|
-
this.messageBuffer += data;
|
|
160
|
-
// Check if the new message buffer contains the statement delimiter.
|
|
161
|
-
while(this.messageBuffer.includes((0, $24139611f53a54b8$export$5d955335434540c6).statementDelimiter)){
|
|
162
|
-
// Split message buffer into statements.
|
|
163
|
-
const statementParts = this.messageBuffer.split((0, $24139611f53a54b8$export$5d955335434540c6).statementDelimiter);
|
|
164
|
-
// For as long as we still have statements to parse..
|
|
165
|
-
while(statementParts.length > 1){
|
|
166
|
-
// Move the first statement to its own variable.
|
|
167
|
-
const currentStatementList = String(statementParts.shift());
|
|
168
|
-
// Parse the statement into an object or list of objects.
|
|
169
|
-
let statementList = (0, $dvphU$parse)(currentStatementList, null, this.options.useBigInt ? (0, $dvphU$parseNumberAndBigInt) : parseFloat);
|
|
170
|
-
// Wrap the statement in an array if it is not already a batched statement list.
|
|
171
|
-
if (!Array.isArray(statementList)) statementList = [
|
|
172
|
-
statementList
|
|
173
|
-
];
|
|
174
|
-
// For as long as there is statements in the result set..
|
|
175
|
-
while(statementList.length > 0){
|
|
176
|
-
// Move the first statement from the batch to its own variable.
|
|
177
|
-
const currentStatement = statementList.shift();
|
|
178
|
-
// If the current statement is a subscription notification..
|
|
179
|
-
if ((0, $abcb763a48577a1e$export$280de919a0cf6928)(currentStatement)) {
|
|
180
|
-
// Emit the notification for handling higher up in the stack.
|
|
181
|
-
this.emit('response', currentStatement);
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
// If the current statement is a version negotiation response..
|
|
185
|
-
if (currentStatement.id === 'versionNegotiation') {
|
|
186
|
-
if ((0, $abcb763a48577a1e$export$d73a2e87a509880)(currentStatement)) // Then emit a failed version negotiation response signal.
|
|
187
|
-
this.emit('version', {
|
|
188
|
-
error: currentStatement.error
|
|
189
|
-
});
|
|
190
|
-
else {
|
|
191
|
-
// Extract the software and protocol version reported.
|
|
192
|
-
const [software, protocol] = currentStatement.result;
|
|
193
|
-
// Emit a successful version negotiation response signal.
|
|
194
|
-
this.emit('version', {
|
|
195
|
-
software: software,
|
|
196
|
-
protocol: protocol
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
// If the current statement is a keep-alive response..
|
|
202
|
-
if (currentStatement.id === 'keepAlive') continue;
|
|
203
|
-
// Emit the statements for handling higher up in the stack.
|
|
204
|
-
this.emit('response', currentStatement);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
// Store the remaining statement as the current message buffer.
|
|
208
|
-
this.messageBuffer = statementParts.shift() || '';
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
/**
|
|
212
|
-
* Sends a keep-alive message to the host.
|
|
213
|
-
*
|
|
214
|
-
* @returns true if the ping message was fully flushed to the socket, false if
|
|
215
|
-
* part of the message is queued in the user memory
|
|
216
|
-
*/ ping() {
|
|
217
|
-
// Write a log message.
|
|
218
|
-
(0, $dvphU$electrumcashdebuglogs).ping(`Sending keep-alive ping to '${this.hostIdentifier}'`);
|
|
219
|
-
// Craft a keep-alive message.
|
|
220
|
-
const message = (0, $24139611f53a54b8$export$5d955335434540c6).buildRequestObject('server.ping', [], 'keepAlive');
|
|
221
|
-
// Send the keep-alive message.
|
|
222
|
-
const status = this.send(message);
|
|
223
|
-
// Return the ping status.
|
|
224
|
-
return status;
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Initiates the network connection negotiates a protocol version. Also emits the 'connect' signal if successful.
|
|
228
|
-
*
|
|
229
|
-
* @throws {Error} if the socket connection fails.
|
|
230
|
-
* @returns a promise resolving when the connection is established
|
|
231
|
-
*/ async connect() {
|
|
232
|
-
// If we are already connected return true.
|
|
233
|
-
if (this.status === (0, $db7c797e63383364$export$7516420eb880ab68).CONNECTED) return;
|
|
234
|
-
// Indicate that the connection is connecting
|
|
235
|
-
this.status = (0, $db7c797e63383364$export$7516420eb880ab68).CONNECTING;
|
|
236
|
-
// Emit a connect event now that the connection is being set up.
|
|
237
|
-
this.emit('connecting');
|
|
238
|
-
// Define a function to wrap connection as a promise.
|
|
239
|
-
const connectionResolver = (resolve, reject)=>{
|
|
240
|
-
const rejector = (error)=>{
|
|
241
|
-
// Set the status back to disconnected
|
|
242
|
-
this.status = (0, $db7c797e63383364$export$7516420eb880ab68).DISCONNECTED;
|
|
243
|
-
// Emit a connect event indicating that we failed to connect.
|
|
244
|
-
this.emit('disconnected');
|
|
245
|
-
// Reject with the error as reason
|
|
246
|
-
reject(error);
|
|
247
|
-
};
|
|
248
|
-
// Replace previous error handlers to reject the promise on failure.
|
|
249
|
-
this.socket.removeAllListeners('error');
|
|
250
|
-
this.socket.once('error', rejector);
|
|
251
|
-
// Define a function to wrap version negotiation as a callback.
|
|
252
|
-
const versionNegotiator = ()=>{
|
|
253
|
-
// Write a log message to show that we have started version negotiation.
|
|
254
|
-
(0, $dvphU$electrumcashdebuglogs).network(`Requesting protocol version ${this.version} with '${this.hostIdentifier}'.`);
|
|
255
|
-
// remove the one-time error handler since no error was detected.
|
|
256
|
-
this.socket.removeListener('error', rejector);
|
|
257
|
-
// Build a version negotiation message.
|
|
258
|
-
const versionMessage = (0, $24139611f53a54b8$export$5d955335434540c6).buildRequestObject('server.version', [
|
|
259
|
-
this.application,
|
|
260
|
-
this.version
|
|
261
|
-
], 'versionNegotiation');
|
|
262
|
-
// Define a function to wrap version validation as a function.
|
|
263
|
-
const versionValidator = (version)=>{
|
|
264
|
-
// Check if version negotiation failed.
|
|
265
|
-
if ((0, $e83d2e7688025acd$export$e1f38ab2b4ebdde6)(version)) {
|
|
266
|
-
// Disconnect from the host.
|
|
267
|
-
this.disconnect(true);
|
|
268
|
-
// Declare an error message.
|
|
269
|
-
const errorMessage = 'unsupported protocol version.';
|
|
270
|
-
// Log the error.
|
|
271
|
-
(0, $dvphU$electrumcashdebuglogs).errors(`Failed to connect with ${this.hostIdentifier} due to ${errorMessage}`);
|
|
272
|
-
// Reject the connection with false since version negotiation failed.
|
|
273
|
-
reject(errorMessage);
|
|
274
|
-
} else if (version.protocol !== this.version && `${version.protocol}.0` !== this.version && `${version.protocol}.0.0` !== this.version) {
|
|
275
|
-
// Disconnect from the host.
|
|
276
|
-
this.disconnect(true);
|
|
277
|
-
// Declare an error message.
|
|
278
|
-
const errorMessage = `incompatible protocol version negotiated (${version.protocol} !== ${this.version}).`;
|
|
279
|
-
// Log the error.
|
|
280
|
-
(0, $dvphU$electrumcashdebuglogs).errors(`Failed to connect with ${this.hostIdentifier} due to ${errorMessage}`);
|
|
281
|
-
// Reject the connection with false since version negotiation failed.
|
|
282
|
-
reject(errorMessage);
|
|
283
|
-
} else {
|
|
284
|
-
// Write a log message.
|
|
285
|
-
(0, $dvphU$electrumcashdebuglogs).network(`Negotiated protocol version ${version.protocol} with '${this.hostIdentifier}', powered by ${version.software}.`);
|
|
286
|
-
// Set connection status to connected
|
|
287
|
-
this.status = (0, $db7c797e63383364$export$7516420eb880ab68).CONNECTED;
|
|
288
|
-
// Emit a connect event now that the connection is usable.
|
|
289
|
-
this.emit('connected');
|
|
290
|
-
// Resolve the connection promise since we successfully connected and negotiated protocol version.
|
|
291
|
-
resolve();
|
|
292
|
-
}
|
|
293
|
-
};
|
|
294
|
-
// Listen for version negotiation once.
|
|
295
|
-
this.once('version', versionValidator);
|
|
296
|
-
// Send the version negotiation message.
|
|
297
|
-
this.send(versionMessage);
|
|
298
|
-
};
|
|
299
|
-
// Prepare the version negotiation.
|
|
300
|
-
this.socket.once('connected', versionNegotiator);
|
|
301
|
-
// Set up handler for network errors.
|
|
302
|
-
this.socket.on('error', this.onSocketError.bind(this));
|
|
303
|
-
// Connect to the server.
|
|
304
|
-
this.socket.connect();
|
|
305
|
-
};
|
|
306
|
-
// Wait until connection is established and version negotiation succeeds.
|
|
307
|
-
await new Promise(connectionResolver);
|
|
308
|
-
}
|
|
309
|
-
/**
|
|
310
|
-
* Restores the network connection.
|
|
311
|
-
*/ async reconnect() {
|
|
312
|
-
// If a reconnect timer is set, remove it
|
|
313
|
-
await this.clearReconnectTimer();
|
|
314
|
-
// Write a log message.
|
|
315
|
-
(0, $dvphU$electrumcashdebuglogs).network(`Trying to reconnect to '${this.hostIdentifier}'..`);
|
|
316
|
-
// Set the status to reconnecting for more accurate log messages.
|
|
317
|
-
this.status = (0, $db7c797e63383364$export$7516420eb880ab68).RECONNECTING;
|
|
318
|
-
// Emit a connect event now that the connection is usable.
|
|
319
|
-
this.emit('reconnecting');
|
|
320
|
-
// Disconnect the underlying socket.
|
|
321
|
-
this.socket.disconnect();
|
|
322
|
-
try {
|
|
323
|
-
// Try to connect again.
|
|
324
|
-
await this.connect();
|
|
325
|
-
} catch (error) {
|
|
326
|
-
// Do nothing as the error should be handled via the disconnect and error signals.
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
/**
|
|
330
|
-
* Removes the current reconnect timer.
|
|
331
|
-
*/ clearReconnectTimer() {
|
|
332
|
-
// If a reconnect timer is set, remove it
|
|
333
|
-
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
334
|
-
// Reset the timer reference.
|
|
335
|
-
this.reconnectTimer = undefined;
|
|
336
|
-
}
|
|
337
|
-
/**
|
|
338
|
-
* Removes the current keep-alive timer.
|
|
339
|
-
*/ clearKeepAliveTimer() {
|
|
340
|
-
// If a keep-alive timer is set, remove it
|
|
341
|
-
if (this.keepAliveTimer) clearTimeout(this.keepAliveTimer);
|
|
342
|
-
// Reset the timer reference.
|
|
343
|
-
this.keepAliveTimer = undefined;
|
|
344
|
-
}
|
|
345
|
-
/**
|
|
346
|
-
* Initializes the keep alive timer loop.
|
|
347
|
-
*/ setupKeepAliveTimer() {
|
|
348
|
-
// If the keep-alive timer loop is not currently set up..
|
|
349
|
-
if (!this.keepAliveTimer) // Set a new keep-alive timer.
|
|
350
|
-
this.keepAliveTimer = setTimeout(this.ping.bind(this), this.options.sendKeepAliveIntervalInMilliSeconds);
|
|
351
|
-
}
|
|
352
|
-
/**
|
|
353
|
-
* Tears down the current connection and removes all event listeners on disconnect.
|
|
354
|
-
*
|
|
355
|
-
* @param force - disconnect even if the connection has not been fully established yet.
|
|
356
|
-
* @param intentional - update connection state if disconnect is intentional.
|
|
357
|
-
*
|
|
358
|
-
* @returns true if successfully disconnected, or false if there was no connection.
|
|
359
|
-
*/ async disconnect(force = false, intentional = true) {
|
|
360
|
-
// Return early when there is nothing to disconnect from
|
|
361
|
-
if (this.status === (0, $db7c797e63383364$export$7516420eb880ab68).DISCONNECTED && !force) // Return false to indicate that there was nothing to disconnect from.
|
|
362
|
-
return false;
|
|
363
|
-
// Update connection state if the disconnection is intentional.
|
|
364
|
-
// NOTE: The state is meant to represent what the client is requesting, but
|
|
365
|
-
// is used internally to handle visibility changes in browsers to ensure functional reconnection.
|
|
366
|
-
if (intentional) // Set connection status to null to indicate tear-down is currently happening.
|
|
367
|
-
this.status = (0, $db7c797e63383364$export$7516420eb880ab68).DISCONNECTING;
|
|
368
|
-
// Emit a connect event to indicate that we are disconnecting.
|
|
369
|
-
this.emit('disconnecting');
|
|
370
|
-
// If a keep-alive timer is set, remove it.
|
|
371
|
-
await this.clearKeepAliveTimer();
|
|
372
|
-
// If a reconnect timer is set, remove it
|
|
373
|
-
await this.clearReconnectTimer();
|
|
374
|
-
const disconnectResolver = (resolve)=>{
|
|
375
|
-
// Resolve to true after the connection emits a disconnect
|
|
376
|
-
this.once('disconnected', ()=>resolve(true));
|
|
377
|
-
// Close the connection on the socket level.
|
|
378
|
-
this.socket.disconnect();
|
|
379
|
-
};
|
|
380
|
-
// Return true to indicate that we disconnected.
|
|
381
|
-
return new Promise(disconnectResolver);
|
|
382
|
-
}
|
|
383
|
-
/**
|
|
384
|
-
* Updates the connection state based on browser reported connectivity.
|
|
385
|
-
*
|
|
386
|
-
* Most modern browsers are able to provide information on the connection state
|
|
387
|
-
* which allows for significantly faster response times to network changes compared
|
|
388
|
-
* to waiting for network requests to fail.
|
|
389
|
-
*
|
|
390
|
-
* When available, we make use of this to fail early to provide a better user experience.
|
|
391
|
-
*/ async handleNetworkChange() {
|
|
392
|
-
// Do nothing if we do not have the navigator available.
|
|
393
|
-
if (typeof window.navigator === 'undefined') return;
|
|
394
|
-
// Attempt to reconnect to the network now that we may be online again.
|
|
395
|
-
if (window.navigator.onLine === true) this.reconnect();
|
|
396
|
-
// Disconnected from the network so that cleanup can happen while we're offline.
|
|
397
|
-
if (window.navigator.onLine !== true) {
|
|
398
|
-
const forceDisconnect = true;
|
|
399
|
-
const isIntentional = true;
|
|
400
|
-
this.disconnect(forceDisconnect, isIntentional);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
/**
|
|
404
|
-
* Updates connection state based on application visibility.
|
|
405
|
-
*
|
|
406
|
-
* Some browsers will disconnect network connections when the browser is out of focus,
|
|
407
|
-
* which would normally cause our reconnect-on-timeout routines to trigger, but that
|
|
408
|
-
* results in a poor user experience since the events are not handled consistently
|
|
409
|
-
* and sometimes it can take some time after restoring focus to the browser.
|
|
410
|
-
*
|
|
411
|
-
* By manually disconnecting when this happens we prevent the default reconnection routines
|
|
412
|
-
* and make the behavior consistent across browsers.
|
|
413
|
-
*/ async handleVisibilityChange() {
|
|
414
|
-
// Disconnect when application is removed from focus.
|
|
415
|
-
if (document.visibilityState === 'hidden') {
|
|
416
|
-
const forceDisconnect = true;
|
|
417
|
-
const isIntentional = true;
|
|
418
|
-
this.disconnect(forceDisconnect, isIntentional);
|
|
419
|
-
}
|
|
420
|
-
// Reconnect when application is returned to focus.
|
|
421
|
-
if (document.visibilityState === 'visible') this.reconnect();
|
|
422
|
-
}
|
|
423
|
-
/**
|
|
424
|
-
* Sends an arbitrary message to the server.
|
|
425
|
-
*
|
|
426
|
-
* @param message - json encoded request object to send to the server, as a string.
|
|
427
|
-
*
|
|
428
|
-
* @returns true if the message was fully flushed to the socket, false if part of the message
|
|
429
|
-
* is queued in the user memory
|
|
430
|
-
*/ send(message) {
|
|
431
|
-
// Remove the current keep-alive timer if it exists.
|
|
432
|
-
this.clearKeepAliveTimer();
|
|
433
|
-
// Get the current timestamp in milliseconds.
|
|
434
|
-
const currentTime = Date.now();
|
|
435
|
-
// Follow up and verify that the message got sent..
|
|
436
|
-
const verificationTimer = setTimeout(this.verifySend.bind(this, currentTime), this.socket.timeout);
|
|
437
|
-
// Store the verification timer locally so that it can be cleared when data has been received.
|
|
438
|
-
this.verifications.push(verificationTimer);
|
|
439
|
-
// Set a new keep-alive timer.
|
|
440
|
-
this.setupKeepAliveTimer();
|
|
441
|
-
// Write the message to the network socket.
|
|
442
|
-
return this.socket.write(message + (0, $24139611f53a54b8$export$5d955335434540c6).statementDelimiter);
|
|
443
|
-
}
|
|
444
|
-
// --- Event managers. --- //
|
|
445
|
-
/**
|
|
446
|
-
* Marks the connection as timed out and schedules reconnection if we have not
|
|
447
|
-
* received data within the expected time frame.
|
|
448
|
-
*/ verifySend(sentTimestamp) {
|
|
449
|
-
// If we haven't received any data since we last sent data out..
|
|
450
|
-
if (Number(this.lastReceivedTimestamp) < sentTimestamp) {
|
|
451
|
-
// If this connection is already disconnected, we do not change anything
|
|
452
|
-
if (this.status === (0, $db7c797e63383364$export$7516420eb880ab68).DISCONNECTED || this.status === (0, $db7c797e63383364$export$7516420eb880ab68).DISCONNECTING) // debug.warning(`Tried to verify already disconnected connection to '${this.hostIdentifier}'`);
|
|
453
|
-
return;
|
|
454
|
-
// Remove the current keep-alive timer if it exists.
|
|
455
|
-
this.clearKeepAliveTimer();
|
|
456
|
-
// Write a notification to the logs.
|
|
457
|
-
(0, $dvphU$electrumcashdebuglogs).network(`Connection to '${this.hostIdentifier}' timed out.`);
|
|
458
|
-
// Close the connection to avoid re-use.
|
|
459
|
-
// NOTE: This initiates reconnection routines if the connection has not
|
|
460
|
-
// been marked as intentionally disconnected.
|
|
461
|
-
this.socket.disconnect();
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
/**
|
|
465
|
-
* Updates the connection status when a connection is confirmed.
|
|
466
|
-
*/ onSocketConnect() {
|
|
467
|
-
// If a reconnect timer is set, remove it.
|
|
468
|
-
this.clearReconnectTimer();
|
|
469
|
-
// Set up the initial timestamp for when we last received data from the server.
|
|
470
|
-
this.lastReceivedTimestamp = Date.now();
|
|
471
|
-
// Set up the initial keep-alive timer.
|
|
472
|
-
this.setupKeepAliveTimer();
|
|
473
|
-
// Clear all temporary error listeners.
|
|
474
|
-
this.socket.removeAllListeners('error');
|
|
475
|
-
// Set up handler for network errors.
|
|
476
|
-
this.socket.on('error', this.onSocketError.bind(this));
|
|
477
|
-
}
|
|
478
|
-
/**
|
|
479
|
-
* Updates the connection status when a connection is ended.
|
|
480
|
-
*/ onSocketDisconnect() {
|
|
481
|
-
// Remove the current keep-alive timer if it exists.
|
|
482
|
-
this.clearKeepAliveTimer();
|
|
483
|
-
// If this is a connection we're trying to tear down..
|
|
484
|
-
if (this.status === (0, $db7c797e63383364$export$7516420eb880ab68).DISCONNECTING) {
|
|
485
|
-
// Mark the connection as disconnected.
|
|
486
|
-
this.status = (0, $db7c797e63383364$export$7516420eb880ab68).DISCONNECTED;
|
|
487
|
-
// Send a disconnect signal higher up the stack.
|
|
488
|
-
this.emit('disconnected');
|
|
489
|
-
// If a reconnect timer is set, remove it.
|
|
490
|
-
this.clearReconnectTimer();
|
|
491
|
-
// Remove all event listeners
|
|
492
|
-
this.removeAllListeners();
|
|
493
|
-
// Write a log message.
|
|
494
|
-
(0, $dvphU$electrumcashdebuglogs).network(`Disconnected from '${this.hostIdentifier}'.`);
|
|
495
|
-
} else {
|
|
496
|
-
// If this is for an established connection..
|
|
497
|
-
if (this.status === (0, $db7c797e63383364$export$7516420eb880ab68).CONNECTED) // Write a notification to the logs.
|
|
498
|
-
(0, $dvphU$electrumcashdebuglogs).errors(`Connection with '${this.hostIdentifier}' was closed, trying to reconnect in ${this.options.reconnectAfterMilliSeconds / 1000} seconds.`);
|
|
499
|
-
// Mark the connection as disconnected for now..
|
|
500
|
-
this.status = (0, $db7c797e63383364$export$7516420eb880ab68).DISCONNECTED;
|
|
501
|
-
// Send a disconnect signal higher up the stack.
|
|
502
|
-
this.emit('disconnected');
|
|
503
|
-
// If we don't have a pending reconnection timer..
|
|
504
|
-
if (!this.reconnectTimer) // Attempt to reconnect after one keep-alive duration.
|
|
505
|
-
this.reconnectTimer = setTimeout(this.reconnect.bind(this), this.options.reconnectAfterMilliSeconds);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
/**
|
|
509
|
-
* Notify administrator of any unexpected errors.
|
|
510
|
-
*/ onSocketError(error) {
|
|
511
|
-
// Report a generic error if no error information is present.
|
|
512
|
-
// NOTE: When using WSS, the error event explicitly
|
|
513
|
-
// only allows to send a "simple" event without data.
|
|
514
|
-
// https://stackoverflow.com/a/18804298
|
|
515
|
-
if (typeof error === 'undefined') // Do nothing, and instead rely on the socket disconnect event for further information.
|
|
516
|
-
return;
|
|
517
|
-
// Log the error, as there is nothing we can do to actually handle it.
|
|
518
|
-
(0, $dvphU$electrumcashdebuglogs).errors(`Network error ('${this.hostIdentifier}'): `, error);
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
// Define number of milliseconds per second for legibility.
|
|
525
|
-
const $d801b1f9b7fc3074$var$MILLI_SECONDS_PER_SECOND = 1000;
|
|
526
|
-
const $d801b1f9b7fc3074$export$5ba3a4134d0d751d = {
|
|
527
|
-
// By default, all numbers including integers are parsed as regular JavaScript numbers.
|
|
528
|
-
useBigInt: false,
|
|
529
|
-
// Send a ping message every seconds, to detect network problem as early as possible.
|
|
530
|
-
sendKeepAliveIntervalInMilliSeconds: 1 * $d801b1f9b7fc3074$var$MILLI_SECONDS_PER_SECOND,
|
|
531
|
-
// Try to reconnect 5 seconds after unintentional disconnects.
|
|
532
|
-
reconnectAfterMilliSeconds: 5 * $d801b1f9b7fc3074$var$MILLI_SECONDS_PER_SECOND,
|
|
533
|
-
// Try to detect stale connections 5 seconds after every send.
|
|
534
|
-
verifyConnectionTimeoutInMilliSeconds: 5 * $d801b1f9b7fc3074$var$MILLI_SECONDS_PER_SECOND,
|
|
535
|
-
// Automatically manage the connection for a consistent behavior across browsers and devices.
|
|
536
|
-
disableBrowserVisibilityHandling: false,
|
|
537
|
-
disableBrowserConnectivityHandling: false
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region source/interfaces.ts
|
|
79
|
+
/**
|
|
80
|
+
* @ignore
|
|
81
|
+
*/
|
|
82
|
+
const isVersionRejected = function(object) {
|
|
83
|
+
return "error" in object;
|
|
538
84
|
};
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
85
|
/**
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
return this.connection.status;
|
|
552
|
-
}
|
|
553
|
-
/**
|
|
554
|
-
* Initializes an Electrum client.
|
|
555
|
-
*
|
|
556
|
-
* @param application - your application name, used to identify to the electrum host.
|
|
557
|
-
* @param version - protocol version to use with the host.
|
|
558
|
-
* @param socketOrHostname - pre-configured electrum socket or fully qualified domain name or IP number of the host
|
|
559
|
-
* @param options - ...
|
|
560
|
-
*
|
|
561
|
-
* @throws {Error} if `version` is not a valid version string.
|
|
562
|
-
*/ constructor(application, version, socketOrHostname, options = {}){
|
|
563
|
-
// Initialize the event emitter.
|
|
564
|
-
super(), this.application = application, this.version = version, this.socketOrHostname = socketOrHostname, this.options = options, this.subscriptionMethods = {}, this.requestId = 0, this.requestResolvers = {}, this.connectionLock = new (0, $dvphU$Mutex)();
|
|
565
|
-
// Update default options with the provided values.
|
|
566
|
-
const networkOptions = {
|
|
567
|
-
...(0, $d801b1f9b7fc3074$export$5ba3a4134d0d751d),
|
|
568
|
-
...options
|
|
569
|
-
};
|
|
570
|
-
// Set up a connection to an electrum server.
|
|
571
|
-
this.connection = new (0, $ff134c9a9e1f7361$export$de0f57fc22079b5e)(application, version, socketOrHostname, networkOptions);
|
|
572
|
-
}
|
|
573
|
-
// Expose hostIdentifier from the connection.
|
|
574
|
-
get hostIdentifier() {
|
|
575
|
-
return this.connection.hostIdentifier;
|
|
576
|
-
}
|
|
577
|
-
// Expose port from the connection.
|
|
578
|
-
get encrypted() {
|
|
579
|
-
return this.connection.encrypted;
|
|
580
|
-
}
|
|
581
|
-
/**
|
|
582
|
-
* Connects to the remote server.
|
|
583
|
-
*
|
|
584
|
-
* @throws {Error} if the socket connection fails.
|
|
585
|
-
* @returns a promise resolving when the connection is established.
|
|
586
|
-
*/ async connect() {
|
|
587
|
-
// Create a lock so that multiple connects/disconnects cannot race each other.
|
|
588
|
-
const unlock = await this.connectionLock.acquire();
|
|
589
|
-
try {
|
|
590
|
-
// If we are already connected, do not attempt to connect again.
|
|
591
|
-
if (this.connection.status === (0, $db7c797e63383364$export$7516420eb880ab68).CONNECTED) return;
|
|
592
|
-
// Listen for parsed statements.
|
|
593
|
-
this.connection.on('response', this.response.bind(this));
|
|
594
|
-
// Hook up handles for the connected and disconnected events.
|
|
595
|
-
this.connection.on('connected', this.resubscribeOnConnect.bind(this));
|
|
596
|
-
this.connection.on('disconnected', this.onConnectionDisconnect.bind(this));
|
|
597
|
-
// Relay connecting and reconnecting events.
|
|
598
|
-
this.connection.on('connecting', this.handleConnectionStatusChanges.bind(this, 'connecting'));
|
|
599
|
-
this.connection.on('disconnecting', this.handleConnectionStatusChanges.bind(this, 'disconnecting'));
|
|
600
|
-
this.connection.on('reconnecting', this.handleConnectionStatusChanges.bind(this, 'reconnecting'));
|
|
601
|
-
// Hook up client metadata gathering functions.
|
|
602
|
-
this.connection.on('version', this.storeSoftwareVersion.bind(this));
|
|
603
|
-
this.connection.on('received', this.updateLastReceivedTimestamp.bind(this));
|
|
604
|
-
// Relay error events.
|
|
605
|
-
this.connection.on('error', this.emit.bind(this, 'error'));
|
|
606
|
-
// Connect with the server.
|
|
607
|
-
await this.connection.connect();
|
|
608
|
-
} finally{
|
|
609
|
-
unlock();
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
/**
|
|
613
|
-
* Disconnects from the remote server and removes all event listeners/subscriptions and open requests.
|
|
614
|
-
*
|
|
615
|
-
* @param force - disconnect even if the connection has not been fully established yet.
|
|
616
|
-
* @param retainSubscriptions - retain subscription data so they will be restored on reconnection.
|
|
617
|
-
*
|
|
618
|
-
* @returns true if successfully disconnected, or false if there was no connection.
|
|
619
|
-
*/ async disconnect(force = false, retainSubscriptions = false) {
|
|
620
|
-
if (!retainSubscriptions) {
|
|
621
|
-
// Cancel all event listeners.
|
|
622
|
-
this.removeAllListeners();
|
|
623
|
-
// Remove all subscription data
|
|
624
|
-
this.subscriptionMethods = {};
|
|
625
|
-
}
|
|
626
|
-
// Disconnect from the remote server.
|
|
627
|
-
return this.connection.disconnect(force);
|
|
628
|
-
}
|
|
629
|
-
/**
|
|
630
|
-
* Calls a method on the remote server with the supplied parameters.
|
|
631
|
-
*
|
|
632
|
-
* @param method - name of the method to call.
|
|
633
|
-
* @param parameters - one or more parameters for the method.
|
|
634
|
-
*
|
|
635
|
-
* @throws {Error} if the client is disconnected.
|
|
636
|
-
* @returns a promise that resolves with the result of the method or an Error.
|
|
637
|
-
*/ async request(method, ...parameters) {
|
|
638
|
-
// If we are not connected to a server..
|
|
639
|
-
if (this.connection.status !== (0, $db7c797e63383364$export$7516420eb880ab68).CONNECTED) // Reject the request with a disconnected error message.
|
|
640
|
-
throw new Error(`Unable to send request to a disconnected server '${this.hostIdentifier}'.`);
|
|
641
|
-
// Increase the request ID by one.
|
|
642
|
-
this.requestId += 1;
|
|
643
|
-
// Store a copy of the request id.
|
|
644
|
-
const id = this.requestId;
|
|
645
|
-
// Format the arguments as an electrum request object.
|
|
646
|
-
const message = (0, $24139611f53a54b8$export$5d955335434540c6).buildRequestObject(method, parameters, id);
|
|
647
|
-
// Define a function to wrap the request in a promise.
|
|
648
|
-
const requestResolver = (resolve)=>{
|
|
649
|
-
// Add a request resolver for this promise to the list of requests.
|
|
650
|
-
this.requestResolvers[id] = (error, data)=>{
|
|
651
|
-
// If the resolution failed..
|
|
652
|
-
if (error) // Resolve the promise with the error for the application to handle.
|
|
653
|
-
resolve(error);
|
|
654
|
-
else // Resolve the promise with the request results.
|
|
655
|
-
resolve(data);
|
|
656
|
-
};
|
|
657
|
-
// Send the request message to the remote server.
|
|
658
|
-
this.connection.send(message);
|
|
659
|
-
};
|
|
660
|
-
// Write a log message.
|
|
661
|
-
(0, $dvphU$electrumcashdebuglogs).network(`Sending request '${method}' to '${this.hostIdentifier}'`);
|
|
662
|
-
// return a promise to deliver results later.
|
|
663
|
-
return new Promise(requestResolver);
|
|
664
|
-
}
|
|
665
|
-
/**
|
|
666
|
-
* Subscribes to the method and payload at the server.
|
|
667
|
-
*
|
|
668
|
-
* @remarks the response for the subscription request is issued as a notification event.
|
|
669
|
-
*
|
|
670
|
-
* @param method - one of the subscribable methods the server supports.
|
|
671
|
-
* @param parameters - one or more parameters for the method.
|
|
672
|
-
*
|
|
673
|
-
* @throws {Error} if the client is disconnected.
|
|
674
|
-
* @returns a promise resolving when the subscription is established.
|
|
675
|
-
*/ async subscribe(method, ...parameters) {
|
|
676
|
-
// Initialize an empty list of subscription payloads, if needed.
|
|
677
|
-
if (!this.subscriptionMethods[method]) this.subscriptionMethods[method] = new Set();
|
|
678
|
-
// Store the subscription parameters to track what data we have subscribed to.
|
|
679
|
-
this.subscriptionMethods[method].add(JSON.stringify(parameters));
|
|
680
|
-
// Send initial subscription request.
|
|
681
|
-
const requestData = await this.request(method, ...parameters);
|
|
682
|
-
// If the request failed, throw it as an error.
|
|
683
|
-
if (requestData instanceof Error) throw requestData;
|
|
684
|
-
// If the request returned more than one data point..
|
|
685
|
-
if (Array.isArray(requestData)) // .. throw an error, as this breaks our expectation for subscriptions.
|
|
686
|
-
throw new Error('Subscription request returned an more than one data point.');
|
|
687
|
-
// Construct a notification structure to package the initial result as a notification.
|
|
688
|
-
const notification = {
|
|
689
|
-
jsonrpc: '2.0',
|
|
690
|
-
method: method,
|
|
691
|
-
params: [
|
|
692
|
-
...parameters,
|
|
693
|
-
requestData
|
|
694
|
-
]
|
|
695
|
-
};
|
|
696
|
-
// Manually emit an event for the initial response.
|
|
697
|
-
this.emit('notification', notification);
|
|
698
|
-
// Try to update the chain height.
|
|
699
|
-
this.updateChainHeightFromHeadersNotifications(notification);
|
|
700
|
-
}
|
|
701
|
-
/**
|
|
702
|
-
* Unsubscribes to the method at the server and removes any callback functions
|
|
703
|
-
* when there are no more subscriptions for the method.
|
|
704
|
-
*
|
|
705
|
-
* @param method - a previously subscribed to method.
|
|
706
|
-
* @param parameters - one or more parameters for the method.
|
|
707
|
-
*
|
|
708
|
-
* @throws {Error} if no subscriptions exist for the combination of the provided `method` and `parameters.
|
|
709
|
-
* @throws {Error} if the client is disconnected.
|
|
710
|
-
* @returns a promise resolving when the subscription is removed.
|
|
711
|
-
*/ async unsubscribe(method, ...parameters) {
|
|
712
|
-
// Throw an error if the client is disconnected.
|
|
713
|
-
if (this.connection.status !== (0, $db7c797e63383364$export$7516420eb880ab68).CONNECTED) throw new Error(`Unable to send unsubscribe request to a disconnected server '${this.hostIdentifier}'.`);
|
|
714
|
-
// If this method has no subscriptions..
|
|
715
|
-
if (!this.subscriptionMethods[method]) // Reject this promise with an explanation.
|
|
716
|
-
throw new Error(`Cannot unsubscribe from '${method}' since the method has no subscriptions.`);
|
|
717
|
-
// Pack up the parameters as a long string.
|
|
718
|
-
const subscriptionParameters = JSON.stringify(parameters);
|
|
719
|
-
// If the method payload could not be located..
|
|
720
|
-
if (!this.subscriptionMethods[method].has(subscriptionParameters)) // Reject this promise with an explanation.
|
|
721
|
-
throw new Error(`Cannot unsubscribe from '${method}' since it has no subscription with the given parameters.`);
|
|
722
|
-
// Remove this specific subscription payload from internal tracking.
|
|
723
|
-
this.subscriptionMethods[method].delete(subscriptionParameters);
|
|
724
|
-
// Send unsubscription request to the server
|
|
725
|
-
// NOTE: As a convenience we allow users to define the method as the subscribe or unsubscribe version.
|
|
726
|
-
await this.request(method.replace('.subscribe', '.unsubscribe'), ...parameters);
|
|
727
|
-
// Write a log message.
|
|
728
|
-
(0, $dvphU$electrumcashdebuglogs).client(`Unsubscribed from '${String(method)}' for the '${subscriptionParameters}' parameters.`);
|
|
729
|
-
}
|
|
730
|
-
/**
|
|
731
|
-
* Restores existing subscriptions without updating status or triggering manual callbacks.
|
|
732
|
-
*
|
|
733
|
-
* @throws {Error} if subscription data cannot be found for all stored event names.
|
|
734
|
-
* @throws {Error} if the client is disconnected.
|
|
735
|
-
* @returns a promise resolving to true when the subscriptions are restored.
|
|
736
|
-
*
|
|
737
|
-
* @ignore
|
|
738
|
-
*/ async resubscribeOnConnect() {
|
|
739
|
-
// Write a log message.
|
|
740
|
-
(0, $dvphU$electrumcashdebuglogs).client(`Connected to '${this.hostIdentifier}'.`);
|
|
741
|
-
// Synchronize with the underlying connection status.
|
|
742
|
-
this.handleConnectionStatusChanges('connected');
|
|
743
|
-
// Initialize an empty list of resubscription promises.
|
|
744
|
-
const resubscriptionPromises = [];
|
|
745
|
-
// For each method we have a subscription for..
|
|
746
|
-
for(const method in this.subscriptionMethods){
|
|
747
|
-
// .. and for each parameter we have previously been subscribed to..
|
|
748
|
-
for (const parameterJSON of this.subscriptionMethods[method].values()){
|
|
749
|
-
// restore the parameters from JSON.
|
|
750
|
-
const parameters = JSON.parse(parameterJSON);
|
|
751
|
-
// Send a subscription request.
|
|
752
|
-
resubscriptionPromises.push(this.subscribe(method, ...parameters));
|
|
753
|
-
}
|
|
754
|
-
// Wait for all re-subscriptions to complete.
|
|
755
|
-
await Promise.all(resubscriptionPromises);
|
|
756
|
-
}
|
|
757
|
-
// Write a log message if there was any subscriptions to restore.
|
|
758
|
-
if (resubscriptionPromises.length > 0) (0, $dvphU$electrumcashdebuglogs).client(`Restored ${resubscriptionPromises.length} previous subscriptions for '${this.hostIdentifier}'`);
|
|
759
|
-
}
|
|
760
|
-
/**
|
|
761
|
-
* Parser messages from the remote server to resolve request promises and emit subscription events.
|
|
762
|
-
*
|
|
763
|
-
* @param message - the response message
|
|
764
|
-
*
|
|
765
|
-
* @throws {Error} if the message ID does not match an existing request.
|
|
766
|
-
* @ignore
|
|
767
|
-
*/ response(message) {
|
|
768
|
-
// If the received message is a notification, we forward it to all event listeners
|
|
769
|
-
if ((0, $abcb763a48577a1e$export$280de919a0cf6928)(message)) {
|
|
770
|
-
// Write a log message.
|
|
771
|
-
(0, $dvphU$electrumcashdebuglogs).client(`Received notification for '${message.method}' from '${this.hostIdentifier}'`);
|
|
772
|
-
// Forward the message content to all event listeners.
|
|
773
|
-
this.emit('notification', message);
|
|
774
|
-
// Try to update the chain height.
|
|
775
|
-
this.updateChainHeightFromHeadersNotifications(message);
|
|
776
|
-
// Return since it does not have an associated request resolver
|
|
777
|
-
return;
|
|
778
|
-
}
|
|
779
|
-
// If the response ID is null we cannot use it to index our request resolvers
|
|
780
|
-
if (message.id === null) // Throw an internal error, this should not happen.
|
|
781
|
-
throw new Error('Internal error: Received an RPC response with ID null.');
|
|
782
|
-
// Look up which request promise we should resolve this.
|
|
783
|
-
const requestResolver = this.requestResolvers[message.id];
|
|
784
|
-
// If we do not have a request resolver for this response message..
|
|
785
|
-
if (!requestResolver) {
|
|
786
|
-
// Log that a message was ignored since the request has already been rejected.
|
|
787
|
-
(0, $dvphU$electrumcashdebuglogs).warning(`Ignoring response #${message.id} as the request has already been rejected.`);
|
|
788
|
-
// Return as this has now been fully handled.
|
|
789
|
-
return;
|
|
790
|
-
}
|
|
791
|
-
// Remove the promise from the request list.
|
|
792
|
-
delete this.requestResolvers[message.id];
|
|
793
|
-
// If the message contains an error..
|
|
794
|
-
if ((0, $abcb763a48577a1e$export$d73a2e87a509880)(message)) // Forward the message error to the request resolver and omit the `result` parameter.
|
|
795
|
-
requestResolver(new Error(message.error.message));
|
|
796
|
-
else {
|
|
797
|
-
// Forward the message content to the request resolver and omit the `error` parameter
|
|
798
|
-
// (by setting it to undefined).
|
|
799
|
-
requestResolver(undefined, message.result);
|
|
800
|
-
// Attempt to extract genesis hash from feature requests.
|
|
801
|
-
this.storeGenesisHashFromFeaturesResponse(message);
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
/**
|
|
805
|
-
* Callback function that is called when connection to the Electrum server is lost.
|
|
806
|
-
* Aborts all active requests with an error message indicating that connection was lost.
|
|
807
|
-
*
|
|
808
|
-
* @ignore
|
|
809
|
-
*/ async onConnectionDisconnect() {
|
|
810
|
-
// Loop over active requests
|
|
811
|
-
for(const resolverId in this.requestResolvers){
|
|
812
|
-
// Extract request resolver for readability
|
|
813
|
-
const requestResolver = this.requestResolvers[resolverId];
|
|
814
|
-
// Resolve the active request with an error indicating that the connection was lost.
|
|
815
|
-
requestResolver(new Error('Connection lost'));
|
|
816
|
-
// Remove the promise from the request list.
|
|
817
|
-
delete this.requestResolvers[resolverId];
|
|
818
|
-
}
|
|
819
|
-
// Synchronize with the underlying connection status.
|
|
820
|
-
this.handleConnectionStatusChanges('disconnected');
|
|
821
|
-
}
|
|
822
|
-
/**
|
|
823
|
-
* Stores the server provider software version field on successful version negotiation.
|
|
824
|
-
*
|
|
825
|
-
* @ignore
|
|
826
|
-
*/ async storeSoftwareVersion(versionStatement) {
|
|
827
|
-
// TODO: handle failed version negotiation better.
|
|
828
|
-
if (versionStatement.error) // Do nothing.
|
|
829
|
-
return;
|
|
830
|
-
// Store the software version.
|
|
831
|
-
this.software = versionStatement.software;
|
|
832
|
-
}
|
|
833
|
-
/**
|
|
834
|
-
* Updates the last received timestamp.
|
|
835
|
-
*
|
|
836
|
-
* @ignore
|
|
837
|
-
*/ async updateLastReceivedTimestamp() {
|
|
838
|
-
// Update the timestamp for when we last received data.
|
|
839
|
-
this.lastReceivedTimestamp = Date.now();
|
|
840
|
-
}
|
|
841
|
-
/**
|
|
842
|
-
* Checks if the provided message is a response to a headers subscription,
|
|
843
|
-
* and if so updates the locally stored chain height value for this client.
|
|
844
|
-
*
|
|
845
|
-
* @ignore
|
|
846
|
-
*/ async updateChainHeightFromHeadersNotifications(message) {
|
|
847
|
-
// If the message is a notification for a new chain height..
|
|
848
|
-
if (message.method === 'blockchain.headers.subscribe') // ..also store the updated chain height locally.
|
|
849
|
-
this.chainHeight = message.params[0].height;
|
|
850
|
-
}
|
|
851
|
-
/**
|
|
852
|
-
* Checks if the provided message is a response to a server.features request,
|
|
853
|
-
* and if so stores the genesis hash for this client locally.
|
|
854
|
-
*
|
|
855
|
-
* @ignore
|
|
856
|
-
*/ async storeGenesisHashFromFeaturesResponse(message) {
|
|
857
|
-
try {
|
|
858
|
-
// If the message is a response to a features request..
|
|
859
|
-
if (typeof message.result.genesis_hash !== 'undefined') // ..store the genesis hash locally.
|
|
860
|
-
this.genesisHash = message.result.genesis_hash;
|
|
861
|
-
} catch (error) {
|
|
862
|
-
// Do nothing.
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
/**
|
|
866
|
-
* Helper function to synchronize state and events with the underlying connection.
|
|
867
|
-
*/ async handleConnectionStatusChanges(eventName) {
|
|
868
|
-
// Re-emit the event.
|
|
869
|
-
this.emit(eventName);
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
var // Export the client.
|
|
873
|
-
$558b46d3f899ced5$export$2e2bcd8739ae039 = $558b46d3f899ced5$var$ElectrumClient;
|
|
874
|
-
|
|
875
|
-
|
|
86
|
+
* @ignore
|
|
87
|
+
*/
|
|
88
|
+
const isVersionNegotiated = function(object) {
|
|
89
|
+
return "software" in object && "protocol" in object;
|
|
90
|
+
};
|
|
876
91
|
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region source/electrum-connection.ts
|
|
94
|
+
/**
|
|
95
|
+
* Wrapper around TLS/WSS sockets that gracefully separates a network stream into Electrum protocol messages.
|
|
96
|
+
*/
|
|
97
|
+
var ElectrumConnection = class extends EventEmitter {
|
|
98
|
+
status = ConnectionStatus.DISCONNECTED;
|
|
99
|
+
lastReceivedTimestamp;
|
|
100
|
+
socket;
|
|
101
|
+
keepAliveTimer;
|
|
102
|
+
reconnectTimer;
|
|
103
|
+
verifications = [];
|
|
104
|
+
messageBuffer = "";
|
|
105
|
+
/**
|
|
106
|
+
* Sets up network configuration for an Electrum client connection.
|
|
107
|
+
*
|
|
108
|
+
* @param application - your application name, used to identify to the electrum host.
|
|
109
|
+
* @param version - protocol version to use with the host.
|
|
110
|
+
* @param socketOrHostname - pre-configured electrum socket or fully qualified domain name or IP number of the host
|
|
111
|
+
* @param options - ...
|
|
112
|
+
*
|
|
113
|
+
* @throws {Error} if `version` is not a valid version string.
|
|
114
|
+
*/
|
|
115
|
+
constructor(application, version, socketOrHostname, options) {
|
|
116
|
+
super();
|
|
117
|
+
this.application = application;
|
|
118
|
+
this.version = version;
|
|
119
|
+
this.socketOrHostname = socketOrHostname;
|
|
120
|
+
this.options = options;
|
|
121
|
+
if (!ElectrumProtocol.versionRegexp.test(version)) throw /* @__PURE__ */ new Error(`Provided version string (${version}) is not a valid protocol version number.`);
|
|
122
|
+
if (typeof socketOrHostname === "string") this.socket = new ElectrumWebSocket(socketOrHostname);
|
|
123
|
+
else this.socket = socketOrHostname;
|
|
124
|
+
this.socket.on("connected", this.onSocketConnect.bind(this));
|
|
125
|
+
this.socket.on("disconnected", this.onSocketDisconnect.bind(this));
|
|
126
|
+
this.socket.on("data", this.parseMessageChunk.bind(this));
|
|
127
|
+
if (typeof document !== "undefined" && !this.options.disableBrowserVisibilityHandling) document.addEventListener("visibilitychange", this.handleVisibilityChange.bind(this));
|
|
128
|
+
if (typeof window !== "undefined" && !this.options.disableBrowserConnectivityHandling) {
|
|
129
|
+
window.addEventListener("online", this.handleNetworkChange.bind(this));
|
|
130
|
+
window.addEventListener("offline", this.handleNetworkChange.bind(this));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
get hostIdentifier() {
|
|
134
|
+
return this.socket.hostIdentifier;
|
|
135
|
+
}
|
|
136
|
+
get encrypted() {
|
|
137
|
+
return this.socket.encrypted;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Assembles incoming data into statements and hands them off to the message parser.
|
|
141
|
+
*
|
|
142
|
+
* @param data - data to append to the current message buffer, as a string.
|
|
143
|
+
*
|
|
144
|
+
* @throws {SyntaxError} if the passed statement parts are not valid JSON.
|
|
145
|
+
*/
|
|
146
|
+
parseMessageChunk(data) {
|
|
147
|
+
this.lastReceivedTimestamp = Date.now();
|
|
148
|
+
this.emit("received");
|
|
149
|
+
this.verifications.forEach((timer) => clearTimeout(timer));
|
|
150
|
+
this.verifications.length = 0;
|
|
151
|
+
this.messageBuffer += data;
|
|
152
|
+
while (this.messageBuffer.includes(ElectrumProtocol.statementDelimiter)) {
|
|
153
|
+
const statementParts = this.messageBuffer.split(ElectrumProtocol.statementDelimiter);
|
|
154
|
+
while (statementParts.length > 1) {
|
|
155
|
+
let statementList = parse(String(statementParts.shift()), null, this.options.useBigInt ? parseNumberAndBigInt : parseFloat);
|
|
156
|
+
if (!Array.isArray(statementList)) statementList = [statementList];
|
|
157
|
+
while (statementList.length > 0) {
|
|
158
|
+
const currentStatement = statementList.shift();
|
|
159
|
+
if (isRPCNotification(currentStatement)) {
|
|
160
|
+
this.emit("response", currentStatement);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (currentStatement.id === "versionNegotiation") {
|
|
164
|
+
if (isRPCErrorResponse(currentStatement)) this.emit("version", { error: currentStatement.error });
|
|
165
|
+
else {
|
|
166
|
+
const [software, protocol] = currentStatement.result;
|
|
167
|
+
this.emit("version", {
|
|
168
|
+
software,
|
|
169
|
+
protocol
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (currentStatement.id === "keepAlive") continue;
|
|
175
|
+
this.emit("response", currentStatement);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
this.messageBuffer = statementParts.shift() || "";
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Sends a keep-alive message to the host.
|
|
183
|
+
*
|
|
184
|
+
* @returns true if the ping message was fully flushed to the socket, false if
|
|
185
|
+
* part of the message is queued in the user memory
|
|
186
|
+
*/
|
|
187
|
+
ping() {
|
|
188
|
+
debug.ping(`Sending keep-alive ping to '${this.hostIdentifier}'`);
|
|
189
|
+
const message = ElectrumProtocol.buildRequestObject("server.ping", [], "keepAlive");
|
|
190
|
+
return this.send(message);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Initiates the network connection negotiates a protocol version. Also emits the 'connect' signal if successful.
|
|
194
|
+
*
|
|
195
|
+
* @throws {Error} if the socket connection fails.
|
|
196
|
+
* @returns a promise resolving when the connection is established
|
|
197
|
+
*/
|
|
198
|
+
async connect() {
|
|
199
|
+
if (this.status === ConnectionStatus.CONNECTED) return;
|
|
200
|
+
this.status = ConnectionStatus.CONNECTING;
|
|
201
|
+
this.emit("connecting");
|
|
202
|
+
const connectionResolver = (resolve, reject) => {
|
|
203
|
+
const rejector = (error) => {
|
|
204
|
+
this.status = ConnectionStatus.DISCONNECTED;
|
|
205
|
+
this.emit("disconnected");
|
|
206
|
+
reject(error);
|
|
207
|
+
};
|
|
208
|
+
this.socket.removeAllListeners("error");
|
|
209
|
+
this.socket.once("error", rejector);
|
|
210
|
+
const versionNegotiator = () => {
|
|
211
|
+
debug.network(`Requesting protocol version ${this.version} with '${this.hostIdentifier}'.`);
|
|
212
|
+
this.socket.removeListener("error", rejector);
|
|
213
|
+
const versionMessage = ElectrumProtocol.buildRequestObject("server.version", [this.application, this.version], "versionNegotiation");
|
|
214
|
+
const versionValidator = (version) => {
|
|
215
|
+
if (isVersionRejected(version)) {
|
|
216
|
+
this.disconnect(true);
|
|
217
|
+
const errorMessage = "unsupported protocol version.";
|
|
218
|
+
debug.errors(`Failed to connect with ${this.hostIdentifier} due to ${errorMessage}`);
|
|
219
|
+
reject(errorMessage);
|
|
220
|
+
} else if (version.protocol !== this.version && `${version.protocol}.0` !== this.version && `${version.protocol}.0.0` !== this.version) {
|
|
221
|
+
this.disconnect(true);
|
|
222
|
+
const errorMessage = `incompatible protocol version negotiated (${version.protocol} !== ${this.version}).`;
|
|
223
|
+
debug.errors(`Failed to connect with ${this.hostIdentifier} due to ${errorMessage}`);
|
|
224
|
+
reject(errorMessage);
|
|
225
|
+
} else {
|
|
226
|
+
debug.network(`Negotiated protocol version ${version.protocol} with '${this.hostIdentifier}', powered by ${version.software}.`);
|
|
227
|
+
this.status = ConnectionStatus.CONNECTED;
|
|
228
|
+
this.emit("connected");
|
|
229
|
+
resolve();
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
this.once("version", versionValidator);
|
|
233
|
+
this.send(versionMessage);
|
|
234
|
+
};
|
|
235
|
+
this.socket.once("connected", versionNegotiator);
|
|
236
|
+
this.socket.on("error", this.onSocketError.bind(this));
|
|
237
|
+
this.socket.connect();
|
|
238
|
+
};
|
|
239
|
+
await new Promise(connectionResolver);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Restores the network connection.
|
|
243
|
+
*/
|
|
244
|
+
async reconnect() {
|
|
245
|
+
await this.clearReconnectTimer();
|
|
246
|
+
debug.network(`Trying to reconnect to '${this.hostIdentifier}'..`);
|
|
247
|
+
this.status = ConnectionStatus.RECONNECTING;
|
|
248
|
+
this.emit("reconnecting");
|
|
249
|
+
this.socket.disconnect();
|
|
250
|
+
try {
|
|
251
|
+
await this.connect();
|
|
252
|
+
} catch (_error) {}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Removes the current reconnect timer.
|
|
256
|
+
*/
|
|
257
|
+
clearReconnectTimer() {
|
|
258
|
+
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
259
|
+
this.reconnectTimer = void 0;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Removes the current keep-alive timer.
|
|
263
|
+
*/
|
|
264
|
+
clearKeepAliveTimer() {
|
|
265
|
+
if (this.keepAliveTimer) clearTimeout(this.keepAliveTimer);
|
|
266
|
+
this.keepAliveTimer = void 0;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Initializes the keep alive timer loop.
|
|
270
|
+
*/
|
|
271
|
+
setupKeepAliveTimer() {
|
|
272
|
+
if (!this.keepAliveTimer) this.keepAliveTimer = setTimeout(this.ping.bind(this), this.options.sendKeepAliveIntervalInMilliSeconds);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Tears down the current connection and removes all event listeners on disconnect.
|
|
276
|
+
*
|
|
277
|
+
* @param force - disconnect even if the connection has not been fully established yet.
|
|
278
|
+
* @param intentional - update connection state if disconnect is intentional.
|
|
279
|
+
*
|
|
280
|
+
* @returns true if successfully disconnected, or false if there was no connection.
|
|
281
|
+
*/
|
|
282
|
+
async disconnect(force = false, intentional = true) {
|
|
283
|
+
if (this.status === ConnectionStatus.DISCONNECTED && !force) return false;
|
|
284
|
+
if (intentional) this.status = ConnectionStatus.DISCONNECTING;
|
|
285
|
+
this.emit("disconnecting");
|
|
286
|
+
await this.clearKeepAliveTimer();
|
|
287
|
+
await this.clearReconnectTimer();
|
|
288
|
+
const disconnectResolver = (resolve) => {
|
|
289
|
+
this.once("disconnected", () => resolve(true));
|
|
290
|
+
this.socket.disconnect();
|
|
291
|
+
};
|
|
292
|
+
return new Promise(disconnectResolver);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Updates the connection state based on browser reported connectivity.
|
|
296
|
+
*
|
|
297
|
+
* Most modern browsers are able to provide information on the connection state
|
|
298
|
+
* which allows for significantly faster response times to network changes compared
|
|
299
|
+
* to waiting for network requests to fail.
|
|
300
|
+
*
|
|
301
|
+
* When available, we make use of this to fail early to provide a better user experience.
|
|
302
|
+
*/
|
|
303
|
+
async handleNetworkChange() {
|
|
304
|
+
if (typeof window.navigator === "undefined") return;
|
|
305
|
+
if (window.navigator.onLine === true) this.reconnect();
|
|
306
|
+
if (window.navigator.onLine !== true) this.disconnect(true, true);
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Updates connection state based on application visibility.
|
|
310
|
+
*
|
|
311
|
+
* Some browsers will disconnect network connections when the browser is out of focus,
|
|
312
|
+
* which would normally cause our reconnect-on-timeout routines to trigger, but that
|
|
313
|
+
* results in a poor user experience since the events are not handled consistently
|
|
314
|
+
* and sometimes it can take some time after restoring focus to the browser.
|
|
315
|
+
*
|
|
316
|
+
* By manually disconnecting when this happens we prevent the default reconnection routines
|
|
317
|
+
* and make the behavior consistent across browsers.
|
|
318
|
+
*/
|
|
319
|
+
async handleVisibilityChange() {
|
|
320
|
+
if (document.visibilityState === "hidden") this.disconnect(true, true);
|
|
321
|
+
if (document.visibilityState === "visible") this.reconnect();
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Sends an arbitrary message to the server.
|
|
325
|
+
*
|
|
326
|
+
* @param message - json encoded request object to send to the server, as a string.
|
|
327
|
+
*
|
|
328
|
+
* @returns true if the message was fully flushed to the socket, false if part of the message
|
|
329
|
+
* is queued in the user memory
|
|
330
|
+
*/
|
|
331
|
+
send(message) {
|
|
332
|
+
this.clearKeepAliveTimer();
|
|
333
|
+
const currentTime = Date.now();
|
|
334
|
+
const verificationTimer = setTimeout(this.verifySend.bind(this, currentTime), this.socket.timeout);
|
|
335
|
+
this.verifications.push(verificationTimer);
|
|
336
|
+
this.setupKeepAliveTimer();
|
|
337
|
+
return this.socket.write(message + ElectrumProtocol.statementDelimiter);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Marks the connection as timed out and schedules reconnection if we have not
|
|
341
|
+
* received data within the expected time frame.
|
|
342
|
+
*/
|
|
343
|
+
verifySend(sentTimestamp) {
|
|
344
|
+
if (Number(this.lastReceivedTimestamp) < sentTimestamp) {
|
|
345
|
+
if (this.status === ConnectionStatus.DISCONNECTED || this.status === ConnectionStatus.DISCONNECTING) return;
|
|
346
|
+
this.clearKeepAliveTimer();
|
|
347
|
+
debug.network(`Connection to '${this.hostIdentifier}' timed out.`);
|
|
348
|
+
this.socket.disconnect();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Updates the connection status when a connection is confirmed.
|
|
353
|
+
*/
|
|
354
|
+
onSocketConnect() {
|
|
355
|
+
this.clearReconnectTimer();
|
|
356
|
+
this.lastReceivedTimestamp = Date.now();
|
|
357
|
+
this.setupKeepAliveTimer();
|
|
358
|
+
this.socket.removeAllListeners("error");
|
|
359
|
+
this.socket.on("error", this.onSocketError.bind(this));
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Updates the connection status when a connection is ended.
|
|
363
|
+
*/
|
|
364
|
+
onSocketDisconnect() {
|
|
365
|
+
this.clearKeepAliveTimer();
|
|
366
|
+
if (this.status === ConnectionStatus.DISCONNECTING) {
|
|
367
|
+
this.status = ConnectionStatus.DISCONNECTED;
|
|
368
|
+
this.emit("disconnected");
|
|
369
|
+
this.clearReconnectTimer();
|
|
370
|
+
this.removeAllListeners();
|
|
371
|
+
debug.network(`Disconnected from '${this.hostIdentifier}'.`);
|
|
372
|
+
} else {
|
|
373
|
+
if (this.status === ConnectionStatus.CONNECTED) debug.errors(`Connection with '${this.hostIdentifier}' was closed, trying to reconnect in ${this.options.reconnectAfterMilliSeconds / 1e3} seconds.`);
|
|
374
|
+
this.status = ConnectionStatus.DISCONNECTED;
|
|
375
|
+
this.emit("disconnected");
|
|
376
|
+
if (!this.reconnectTimer) this.reconnectTimer = setTimeout(this.reconnect.bind(this), this.options.reconnectAfterMilliSeconds);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Notify administrator of any unexpected errors.
|
|
381
|
+
*/
|
|
382
|
+
onSocketError(error) {
|
|
383
|
+
if (typeof error === "undefined") return;
|
|
384
|
+
debug.errors(`Network error ('${this.hostIdentifier}'): `, error);
|
|
385
|
+
}
|
|
386
|
+
};
|
|
877
387
|
|
|
388
|
+
//#endregion
|
|
389
|
+
//#region source/constants.ts
|
|
390
|
+
const MILLI_SECONDS_PER_SECOND = 1e3;
|
|
391
|
+
/**
|
|
392
|
+
* Configure default options.
|
|
393
|
+
*/
|
|
394
|
+
const defaultNetworkOptions = {
|
|
395
|
+
useBigInt: false,
|
|
396
|
+
sendKeepAliveIntervalInMilliSeconds: 1 * MILLI_SECONDS_PER_SECOND,
|
|
397
|
+
reconnectAfterMilliSeconds: 5 * MILLI_SECONDS_PER_SECOND,
|
|
398
|
+
verifyConnectionTimeoutInMilliSeconds: 5 * MILLI_SECONDS_PER_SECOND,
|
|
399
|
+
disableBrowserVisibilityHandling: false,
|
|
400
|
+
disableBrowserConnectivityHandling: false
|
|
401
|
+
};
|
|
878
402
|
|
|
403
|
+
//#endregion
|
|
404
|
+
//#region source/electrum-client.ts
|
|
405
|
+
/**
|
|
406
|
+
* High-level Electrum client that lets applications send requests and subscribe to notification events from a server.
|
|
407
|
+
*/
|
|
408
|
+
var ElectrumClient = class extends EventEmitter {
|
|
409
|
+
/**
|
|
410
|
+
* The name and version of the server software indexing the blockchain.
|
|
411
|
+
*/
|
|
412
|
+
software;
|
|
413
|
+
/**
|
|
414
|
+
* The genesis hash of the blockchain indexed by the server.
|
|
415
|
+
* @remarks This is only available after a 'server.features' call.
|
|
416
|
+
*/
|
|
417
|
+
genesisHash;
|
|
418
|
+
/**
|
|
419
|
+
* The chain height of the blockchain indexed by the server.
|
|
420
|
+
* @remarks This is only available after a 'blockchain.headers.subscribe' call.
|
|
421
|
+
*/
|
|
422
|
+
chainHeight;
|
|
423
|
+
/**
|
|
424
|
+
* Timestamp of when we last received data from the server indexing the blockchain.
|
|
425
|
+
*/
|
|
426
|
+
lastReceivedTimestamp;
|
|
427
|
+
/**
|
|
428
|
+
* Number corresponding to the underlying connection status.
|
|
429
|
+
*/
|
|
430
|
+
get status() {
|
|
431
|
+
return this.connection.status;
|
|
432
|
+
}
|
|
433
|
+
connection;
|
|
434
|
+
subscriptionMethods = {};
|
|
435
|
+
requestId = 0;
|
|
436
|
+
requestResolvers = {};
|
|
437
|
+
connectionLock = new Mutex();
|
|
438
|
+
/**
|
|
439
|
+
* Initializes an Electrum client.
|
|
440
|
+
*
|
|
441
|
+
* @param application - your application name, used to identify to the electrum host.
|
|
442
|
+
* @param version - protocol version to use with the host.
|
|
443
|
+
* @param socketOrHostname - pre-configured electrum socket or fully qualified domain name or IP number of the host
|
|
444
|
+
* @param options - ...
|
|
445
|
+
*
|
|
446
|
+
* @throws {Error} if `version` is not a valid version string.
|
|
447
|
+
*/
|
|
448
|
+
constructor(application, version, socketOrHostname, options = {}) {
|
|
449
|
+
super();
|
|
450
|
+
this.application = application;
|
|
451
|
+
this.version = version;
|
|
452
|
+
this.socketOrHostname = socketOrHostname;
|
|
453
|
+
this.options = options;
|
|
454
|
+
this.connection = new ElectrumConnection(application, version, socketOrHostname, {
|
|
455
|
+
...defaultNetworkOptions,
|
|
456
|
+
...options
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
get hostIdentifier() {
|
|
460
|
+
return this.connection.hostIdentifier;
|
|
461
|
+
}
|
|
462
|
+
get encrypted() {
|
|
463
|
+
return this.connection.encrypted;
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Connects to the remote server.
|
|
467
|
+
*
|
|
468
|
+
* @throws {Error} if the socket connection fails.
|
|
469
|
+
* @returns a promise resolving when the connection is established.
|
|
470
|
+
*/
|
|
471
|
+
async connect() {
|
|
472
|
+
const unlock = await this.connectionLock.acquire();
|
|
473
|
+
try {
|
|
474
|
+
if (this.connection.status === ConnectionStatus.CONNECTED) return;
|
|
475
|
+
this.connection.on("response", this.response.bind(this));
|
|
476
|
+
this.connection.on("connected", this.resubscribeOnConnect.bind(this));
|
|
477
|
+
this.connection.on("disconnected", this.onConnectionDisconnect.bind(this));
|
|
478
|
+
this.connection.on("connecting", this.handleConnectionStatusChanges.bind(this, "connecting"));
|
|
479
|
+
this.connection.on("disconnecting", this.handleConnectionStatusChanges.bind(this, "disconnecting"));
|
|
480
|
+
this.connection.on("reconnecting", this.handleConnectionStatusChanges.bind(this, "reconnecting"));
|
|
481
|
+
this.connection.on("version", this.storeSoftwareVersion.bind(this));
|
|
482
|
+
this.connection.on("received", this.updateLastReceivedTimestamp.bind(this));
|
|
483
|
+
this.connection.on("error", this.emit.bind(this, "error"));
|
|
484
|
+
await this.connection.connect();
|
|
485
|
+
} finally {
|
|
486
|
+
unlock();
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Disconnects from the remote server and removes all event listeners/subscriptions and open requests.
|
|
491
|
+
*
|
|
492
|
+
* @param force - disconnect even if the connection has not been fully established yet.
|
|
493
|
+
* @param retainSubscriptions - retain subscription data so they will be restored on reconnection.
|
|
494
|
+
*
|
|
495
|
+
* @returns true if successfully disconnected, or false if there was no connection.
|
|
496
|
+
*/
|
|
497
|
+
async disconnect(force = false, retainSubscriptions = false) {
|
|
498
|
+
if (!retainSubscriptions) {
|
|
499
|
+
this.removeAllListeners();
|
|
500
|
+
this.subscriptionMethods = {};
|
|
501
|
+
}
|
|
502
|
+
return this.connection.disconnect(force);
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Calls a method on the remote server with the supplied parameters.
|
|
506
|
+
*
|
|
507
|
+
* @param method - name of the method to call.
|
|
508
|
+
* @param parameters - one or more parameters for the method.
|
|
509
|
+
*
|
|
510
|
+
* @throws {Error} if the client is disconnected.
|
|
511
|
+
* @returns a promise that resolves with the result of the method or an Error.
|
|
512
|
+
*/
|
|
513
|
+
async request(method, ...parameters) {
|
|
514
|
+
if (this.connection.status !== ConnectionStatus.CONNECTED) throw /* @__PURE__ */ new Error(`Unable to send request to a disconnected server '${this.hostIdentifier}'.`);
|
|
515
|
+
this.requestId += 1;
|
|
516
|
+
const id = this.requestId;
|
|
517
|
+
const message = ElectrumProtocol.buildRequestObject(method, parameters, id);
|
|
518
|
+
const requestResolver = (resolve) => {
|
|
519
|
+
this.requestResolvers[id] = (error, data) => {
|
|
520
|
+
if (error) resolve(error);
|
|
521
|
+
else resolve(data);
|
|
522
|
+
};
|
|
523
|
+
this.connection.send(message);
|
|
524
|
+
};
|
|
525
|
+
debug.network(`Sending request '${method}' to '${this.hostIdentifier}'`);
|
|
526
|
+
return new Promise(requestResolver);
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Subscribes to the method and payload at the server.
|
|
530
|
+
*
|
|
531
|
+
* @remarks the response for the subscription request is issued as a notification event.
|
|
532
|
+
*
|
|
533
|
+
* @param method - one of the subscribable methods the server supports.
|
|
534
|
+
* @param parameters - one or more parameters for the method.
|
|
535
|
+
*
|
|
536
|
+
* @throws {Error} if the client is disconnected.
|
|
537
|
+
* @returns a promise resolving when the subscription is established.
|
|
538
|
+
*/
|
|
539
|
+
async subscribe(method, ...parameters) {
|
|
540
|
+
if (!this.subscriptionMethods[method]) this.subscriptionMethods[method] = /* @__PURE__ */ new Set();
|
|
541
|
+
this.subscriptionMethods[method].add(JSON.stringify(parameters));
|
|
542
|
+
const requestData = await this.request(method, ...parameters);
|
|
543
|
+
if (requestData instanceof Error) throw requestData;
|
|
544
|
+
if (Array.isArray(requestData)) throw /* @__PURE__ */ new Error("Subscription request returned an more than one data point.");
|
|
545
|
+
const notification = {
|
|
546
|
+
jsonrpc: "2.0",
|
|
547
|
+
method,
|
|
548
|
+
params: [...parameters, requestData]
|
|
549
|
+
};
|
|
550
|
+
this.emit("notification", notification);
|
|
551
|
+
this.updateChainHeightFromHeadersNotifications(notification);
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Unsubscribes to the method at the server and removes any callback functions
|
|
555
|
+
* when there are no more subscriptions for the method.
|
|
556
|
+
*
|
|
557
|
+
* @param method - a previously subscribed to method.
|
|
558
|
+
* @param parameters - one or more parameters for the method.
|
|
559
|
+
*
|
|
560
|
+
* @throws {Error} if no subscriptions exist for the combination of the provided `method` and `parameters.
|
|
561
|
+
* @throws {Error} if the client is disconnected.
|
|
562
|
+
* @returns a promise resolving when the subscription is removed.
|
|
563
|
+
*/
|
|
564
|
+
async unsubscribe(method, ...parameters) {
|
|
565
|
+
if (this.connection.status !== ConnectionStatus.CONNECTED) throw /* @__PURE__ */ new Error(`Unable to send unsubscribe request to a disconnected server '${this.hostIdentifier}'.`);
|
|
566
|
+
if (!this.subscriptionMethods[method]) throw /* @__PURE__ */ new Error(`Cannot unsubscribe from '${method}' since the method has no subscriptions.`);
|
|
567
|
+
const subscriptionParameters = JSON.stringify(parameters);
|
|
568
|
+
if (!this.subscriptionMethods[method].has(subscriptionParameters)) throw /* @__PURE__ */ new Error(`Cannot unsubscribe from '${method}' since it has no subscription with the given parameters.`);
|
|
569
|
+
this.subscriptionMethods[method].delete(subscriptionParameters);
|
|
570
|
+
await this.request(method.replace(".subscribe", ".unsubscribe"), ...parameters);
|
|
571
|
+
debug.client(`Unsubscribed from '${String(method)}' for the '${subscriptionParameters}' parameters.`);
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Restores existing subscriptions without updating status or triggering manual callbacks.
|
|
575
|
+
*
|
|
576
|
+
* @throws {Error} if subscription data cannot be found for all stored event names.
|
|
577
|
+
* @throws {Error} if the client is disconnected.
|
|
578
|
+
* @returns a promise resolving to true when the subscriptions are restored.
|
|
579
|
+
*
|
|
580
|
+
* @ignore
|
|
581
|
+
*/
|
|
582
|
+
async resubscribeOnConnect() {
|
|
583
|
+
debug.client(`Connected to '${this.hostIdentifier}'.`);
|
|
584
|
+
this.handleConnectionStatusChanges("connected");
|
|
585
|
+
const resubscriptionPromises = [];
|
|
586
|
+
for (const method in this.subscriptionMethods) {
|
|
587
|
+
for (const parameterJSON of this.subscriptionMethods[method].values()) {
|
|
588
|
+
const parameters = JSON.parse(parameterJSON);
|
|
589
|
+
resubscriptionPromises.push(this.subscribe(method, ...parameters));
|
|
590
|
+
}
|
|
591
|
+
await Promise.all(resubscriptionPromises);
|
|
592
|
+
}
|
|
593
|
+
if (resubscriptionPromises.length > 0) debug.client(`Restored ${resubscriptionPromises.length} previous subscriptions for '${this.hostIdentifier}'`);
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Parser messages from the remote server to resolve request promises and emit subscription events.
|
|
597
|
+
*
|
|
598
|
+
* @param message - the response message
|
|
599
|
+
*
|
|
600
|
+
* @throws {Error} if the message ID does not match an existing request.
|
|
601
|
+
* @ignore
|
|
602
|
+
*/
|
|
603
|
+
response(message) {
|
|
604
|
+
if (isRPCNotification(message)) {
|
|
605
|
+
debug.client(`Received notification for '${message.method}' from '${this.hostIdentifier}'`);
|
|
606
|
+
this.emit("notification", message);
|
|
607
|
+
this.updateChainHeightFromHeadersNotifications(message);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
if (message.id === null) throw /* @__PURE__ */ new Error("Internal error: Received an RPC response with ID null.");
|
|
611
|
+
const requestResolver = this.requestResolvers[message.id];
|
|
612
|
+
if (!requestResolver) {
|
|
613
|
+
debug.warning(`Ignoring response #${message.id} as the request has already been rejected.`);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
delete this.requestResolvers[message.id];
|
|
617
|
+
if (isRPCErrorResponse(message)) requestResolver(new Error(message.error.message));
|
|
618
|
+
else {
|
|
619
|
+
requestResolver(void 0, message.result);
|
|
620
|
+
this.storeGenesisHashFromFeaturesResponse(message);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Callback function that is called when connection to the Electrum server is lost.
|
|
625
|
+
* Aborts all active requests with an error message indicating that connection was lost.
|
|
626
|
+
*
|
|
627
|
+
* @ignore
|
|
628
|
+
*/
|
|
629
|
+
async onConnectionDisconnect() {
|
|
630
|
+
for (const resolverId in this.requestResolvers) {
|
|
631
|
+
const requestResolver = this.requestResolvers[resolverId];
|
|
632
|
+
requestResolver(/* @__PURE__ */ new Error("Connection lost"));
|
|
633
|
+
delete this.requestResolvers[resolverId];
|
|
634
|
+
}
|
|
635
|
+
this.handleConnectionStatusChanges("disconnected");
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Stores the server provider software version field on successful version negotiation.
|
|
639
|
+
*
|
|
640
|
+
* @ignore
|
|
641
|
+
*/
|
|
642
|
+
async storeSoftwareVersion(versionStatement) {
|
|
643
|
+
if (versionStatement.error) return;
|
|
644
|
+
this.software = versionStatement.software;
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Updates the last received timestamp.
|
|
648
|
+
*
|
|
649
|
+
* @ignore
|
|
650
|
+
*/
|
|
651
|
+
async updateLastReceivedTimestamp() {
|
|
652
|
+
this.lastReceivedTimestamp = Date.now();
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Checks if the provided message is a response to a headers subscription,
|
|
656
|
+
* and if so updates the locally stored chain height value for this client.
|
|
657
|
+
*
|
|
658
|
+
* @ignore
|
|
659
|
+
*/
|
|
660
|
+
async updateChainHeightFromHeadersNotifications(message) {
|
|
661
|
+
if (message.method === "blockchain.headers.subscribe") this.chainHeight = message.params[0].height;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Checks if the provided message is a response to a server.features request,
|
|
665
|
+
* and if so stores the genesis hash for this client locally.
|
|
666
|
+
*
|
|
667
|
+
* @ignore
|
|
668
|
+
*/
|
|
669
|
+
async storeGenesisHashFromFeaturesResponse(message) {
|
|
670
|
+
try {
|
|
671
|
+
if (typeof message.result.genesis_hash !== "undefined") this.genesisHash = message.result.genesis_hash;
|
|
672
|
+
} catch (_ignored) {}
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Helper function to synchronize state and events with the underlying connection.
|
|
676
|
+
*/
|
|
677
|
+
async handleConnectionStatusChanges(eventName) {
|
|
678
|
+
this.emit(eventName);
|
|
679
|
+
}
|
|
680
|
+
connecting;
|
|
681
|
+
connected;
|
|
682
|
+
disconnecting;
|
|
683
|
+
disconnected;
|
|
684
|
+
reconnecting;
|
|
685
|
+
notification;
|
|
686
|
+
error;
|
|
687
|
+
};
|
|
688
|
+
var electrum_client_default = ElectrumClient;
|
|
879
689
|
|
|
880
|
-
|
|
881
|
-
|
|
690
|
+
//#endregion
|
|
691
|
+
export { ConnectionStatus, electrum_client_default as ElectrumClient, isVersionNegotiated, isVersionRejected };
|
|
692
|
+
//# sourceMappingURL=index.mjs.map
|