@electrum-cash/network
Version:
@electrum-cash/network is a lightweight JavaScript library that lets you connect with one or more Electrum servers.
897 lines (864 loc) • 46.1 kB
JavaScript
import $dvphU$electrumcashdebuglogs from "@electrum-cash/debug-logs";
import {EventEmitter as $dvphU$EventEmitter} from "eventemitter3";
import {Mutex as $dvphU$Mutex} from "async-mutex";
import {ElectrumWebSocket as $dvphU$ElectrumWebSocket} from "@electrum-cash/web-socket";
import {parse as $dvphU$parse, parseNumberAndBigInt as $dvphU$parseNumberAndBigInt} from "lossless-json";
function $parcel$export(e, n, v, s) {
Object.defineProperty(e, n, {get: v, set: s, enumerable: true, configurable: true});
}
class $24139611f53a54b8$export$5d955335434540c6 {
/**
* Helper function that builds an Electrum request object.
*
* @param method - method to call.
* @param parameters - method parameters for the call.
* @param requestId - unique string or number referencing this request.
*
* @returns a properly formatted Electrum request string.
*/ static buildRequestObject(method, parameters, requestId) {
// Return the formatted request object.
// NOTE: Electrum either uses JsonRPC strictly or loosely.
// If we specify protocol identifier without being 100% compliant, we risk being disconnected/blacklisted.
// For this reason, we omit the protocol identifier to avoid issues.
return JSON.stringify({
method: method,
params: parameters,
id: requestId
});
}
/**
* Constant used to verify if a provided string is a valid version number.
*
* @returns a regular expression that matches valid version numbers.
*/ static get versionRegexp() {
return /^\d+(\.\d+)+$/;
}
/**
* Constant used to separate statements/messages in a stream of data.
*
* @returns the delimiter used by Electrum to separate statements.
*/ static get statementDelimiter() {
return "\n";
}
}
var $e83d2e7688025acd$exports = {};
$parcel$export($e83d2e7688025acd$exports, "isVersionRejected", () => $e83d2e7688025acd$export$e1f38ab2b4ebdde6);
$parcel$export($e83d2e7688025acd$exports, "isVersionNegotiated", () => $e83d2e7688025acd$export$9598f0c76aa41d73);
const $e83d2e7688025acd$export$e1f38ab2b4ebdde6 = function(object) {
return "error" in object;
};
const $e83d2e7688025acd$export$9598f0c76aa41d73 = function(object) {
return "software" in object && "protocol" in object;
};
// Acceptable parameter types for RPC messages
const $abcb763a48577a1e$export$d73a2e87a509880 = function(message) {
return "id" in message && "error" in message;
};
const $abcb763a48577a1e$export$81276773828ff315 = function(message) {
return "id" in message && "result" in message;
};
const $abcb763a48577a1e$export$280de919a0cf6928 = function(message) {
return !("id" in message) && "method" in message;
};
const $abcb763a48577a1e$export$94e3360fcddccc76 = function(message) {
return "id" in message && "method" in message;
};
var $db7c797e63383364$exports = {};
$parcel$export($db7c797e63383364$exports, "ConnectionStatus", () => $db7c797e63383364$export$7516420eb880ab68);
// Disable indent rule for this file because it is broken (https://github.com/typescript-eslint/typescript-eslint/issues/1824)
/* eslint-disable @typescript-eslint/indent */ /**
* Enum that denotes the connection status of an ElectrumConnection.
* @enum {number}
* @property {0} DISCONNECTED The connection is disconnected.
* @property {1} AVAILABLE The connection is connected.
* @property {2} DISCONNECTING The connection is disconnecting.
* @property {3} CONNECTING The connection is connecting.
* @property {4} RECONNECTING The connection is restarting.
*/ var $db7c797e63383364$export$7516420eb880ab68;
(function(ConnectionStatus) {
ConnectionStatus[ConnectionStatus["DISCONNECTED"] = 0] = "DISCONNECTED";
ConnectionStatus[ConnectionStatus["CONNECTED"] = 1] = "CONNECTED";
ConnectionStatus[ConnectionStatus["DISCONNECTING"] = 2] = "DISCONNECTING";
ConnectionStatus[ConnectionStatus["CONNECTING"] = 3] = "CONNECTING";
ConnectionStatus[ConnectionStatus["RECONNECTING"] = 4] = "RECONNECTING";
})($db7c797e63383364$export$7516420eb880ab68 || ($db7c797e63383364$export$7516420eb880ab68 = {}));
class $ff134c9a9e1f7361$export$de0f57fc22079b5e extends (0, $dvphU$EventEmitter) {
/**
* Sets up network configuration for an Electrum client connection.
*
* @param application - your application name, used to identify to the electrum host.
* @param version - protocol version to use with the host.
* @param socketOrHostname - pre-configured electrum socket or fully qualified domain name or IP number of the host
* @param options - ...
*
* @throws {Error} if `version` is not a valid version string.
*/ constructor(application, version, socketOrHostname, options){
// Initialize the event emitter.
super();
this.application = application;
this.version = version;
this.socketOrHostname = socketOrHostname;
this.options = options;
this.status = (0, $db7c797e63383364$export$7516420eb880ab68).DISCONNECTED;
this.verifications = [];
this.messageBuffer = "";
// Check if the provided version is a valid version number.
if (!(0, $24139611f53a54b8$export$5d955335434540c6).versionRegexp.test(version)) // Throw an error since the version number was not valid.
throw new Error(`Provided version string (${version}) is not a valid protocol version number.`);
// If a hostname was provided..
if (typeof socketOrHostname === "string") // Use a web socket with default parameters.
this.socket = new (0, $dvphU$ElectrumWebSocket)(socketOrHostname);
else // Use the provided socket.
this.socket = socketOrHostname;
// Set up handlers for connection and disconnection.
this.socket.on("connected", this.onSocketConnect.bind(this));
this.socket.on("disconnected", this.onSocketDisconnect.bind(this));
// Set up handler for incoming data.
this.socket.on("data", this.parseMessageChunk.bind(this));
// Handle visibility changes when run in a browser environment (if not explicitly disabled).
if (typeof document !== "undefined" && !this.options.disableBrowserVisibilityHandling) document.addEventListener("visibilitychange", this.handleVisibilityChange.bind(this));
// Handle network connection changes when run in a browser environment (if not explicitly disabled).
if (typeof window !== "undefined" && !this.options.disableBrowserConnectivityHandling) {
window.addEventListener("online", this.handleNetworkChange.bind(this));
window.addEventListener("offline", this.handleNetworkChange.bind(this));
}
}
// Expose hostIdentifier from the socket.
get hostIdentifier() {
return this.socket.hostIdentifier;
}
// Expose port from the socket.
get encrypted() {
return this.socket.encrypted;
}
/**
* Assembles incoming data into statements and hands them off to the message parser.
*
* @param data - data to append to the current message buffer, as a string.
*
* @throws {SyntaxError} if the passed statement parts are not valid JSON.
*/ parseMessageChunk(data) {
// Update the timestamp for when we last received data.
this.lastReceivedTimestamp = Date.now();
// Emit a notification indicating that the connection has received data.
this.emit("received");
// Clear and remove all verification timers.
this.verifications.forEach((timer)=>clearTimeout(timer));
this.verifications.length = 0;
// Add the message to the current message buffer.
this.messageBuffer += data;
// Check if the new message buffer contains the statement delimiter.
while(this.messageBuffer.includes((0, $24139611f53a54b8$export$5d955335434540c6).statementDelimiter)){
// Split message buffer into statements.
const statementParts = this.messageBuffer.split((0, $24139611f53a54b8$export$5d955335434540c6).statementDelimiter);
// For as long as we still have statements to parse..
while(statementParts.length > 1){
// Move the first statement to its own variable.
const currentStatementList = String(statementParts.shift());
// Parse the statement into an object or list of objects.
let statementList = (0, $dvphU$parse)(currentStatementList, null, this.options.useBigInt ? (0, $dvphU$parseNumberAndBigInt) : parseFloat);
// Wrap the statement in an array if it is not already a batched statement list.
if (!Array.isArray(statementList)) statementList = [
statementList
];
// For as long as there is statements in the result set..
while(statementList.length > 0){
// Move the first statement from the batch to its own variable.
const currentStatement = statementList.shift();
// If the current statement is a subscription notification..
if ((0, $abcb763a48577a1e$export$280de919a0cf6928)(currentStatement)) {
// Emit the notification for handling higher up in the stack.
this.emit("response", currentStatement);
continue;
}
// If the current statement is a version negotiation response..
if (currentStatement.id === "versionNegotiation") {
if ((0, $abcb763a48577a1e$export$d73a2e87a509880)(currentStatement)) // Then emit a failed version negotiation response signal.
this.emit("version", {
error: currentStatement.error
});
else {
// Extract the software and protocol version reported.
const [software, protocol] = currentStatement.result;
// Emit a successful version negotiation response signal.
this.emit("version", {
software: software,
protocol: protocol
});
}
continue;
}
// If the current statement is a keep-alive response..
if (currentStatement.id === "keepAlive") continue;
// Emit the statements for handling higher up in the stack.
this.emit("response", currentStatement);
}
}
// Store the remaining statement as the current message buffer.
this.messageBuffer = statementParts.shift() || "";
}
}
/**
* Sends a keep-alive message to the host.
*
* @returns true if the ping message was fully flushed to the socket, false if
* part of the message is queued in the user memory
*/ ping() {
// Write a log message.
(0, $dvphU$electrumcashdebuglogs).ping(`Sending keep-alive ping to '${this.hostIdentifier}'`);
// Craft a keep-alive message.
const message = (0, $24139611f53a54b8$export$5d955335434540c6).buildRequestObject("server.ping", [], "keepAlive");
// Send the keep-alive message.
const status = this.send(message);
// Return the ping status.
return status;
}
/**
* Initiates the network connection negotiates a protocol version. Also emits the 'connect' signal if successful.
*
* @throws {Error} if the socket connection fails.
* @returns a promise resolving when the connection is established
*/ async connect() {
// If we are already connected return true.
if (this.status === (0, $db7c797e63383364$export$7516420eb880ab68).CONNECTED) return;
// Indicate that the connection is connecting
this.status = (0, $db7c797e63383364$export$7516420eb880ab68).CONNECTING;
// Emit a connect event now that the connection is being set up.
this.emit("connecting");
// Define a function to wrap connection as a promise.
const connectionResolver = (resolve, reject)=>{
const rejector = (error)=>{
// Set the status back to disconnected
this.status = (0, $db7c797e63383364$export$7516420eb880ab68).DISCONNECTED;
// Emit a connect event indicating that we failed to connect.
this.emit("disconnected");
// Reject with the error as reason
reject(error);
};
// Replace previous error handlers to reject the promise on failure.
this.socket.removeAllListeners("error");
this.socket.once("error", rejector);
// Define a function to wrap version negotiation as a callback.
const versionNegotiator = ()=>{
// Write a log message to show that we have started version negotiation.
(0, $dvphU$electrumcashdebuglogs).network(`Requesting protocol version ${this.version} with '${this.hostIdentifier}'.`);
// remove the one-time error handler since no error was detected.
this.socket.removeListener("error", rejector);
// Build a version negotiation message.
const versionMessage = (0, $24139611f53a54b8$export$5d955335434540c6).buildRequestObject("server.version", [
this.application,
this.version
], "versionNegotiation");
// Define a function to wrap version validation as a function.
const versionValidator = (version)=>{
// Check if version negotiation failed.
if ((0, $e83d2e7688025acd$export$e1f38ab2b4ebdde6)(version)) {
// Disconnect from the host.
this.disconnect(true);
// Declare an error message.
const errorMessage = "unsupported protocol version.";
// Log the error.
(0, $dvphU$electrumcashdebuglogs).errors(`Failed to connect with ${this.hostIdentifier} due to ${errorMessage}`);
// Reject the connection with false since version negotiation failed.
reject(errorMessage);
} else if (version.protocol !== this.version && `${version.protocol}.0` !== this.version && `${version.protocol}.0.0` !== this.version) {
// Disconnect from the host.
this.disconnect(true);
// Declare an error message.
const errorMessage = `incompatible protocol version negotiated (${version.protocol} !== ${this.version}).`;
// Log the error.
(0, $dvphU$electrumcashdebuglogs).errors(`Failed to connect with ${this.hostIdentifier} due to ${errorMessage}`);
// Reject the connection with false since version negotiation failed.
reject(errorMessage);
} else {
// Write a log message.
(0, $dvphU$electrumcashdebuglogs).network(`Negotiated protocol version ${version.protocol} with '${this.hostIdentifier}', powered by ${version.software}.`);
// Set connection status to connected
this.status = (0, $db7c797e63383364$export$7516420eb880ab68).CONNECTED;
// Emit a connect event now that the connection is usable.
this.emit("connected");
// Resolve the connection promise since we successfully connected and negotiated protocol version.
resolve();
}
};
// Listen for version negotiation once.
this.once("version", versionValidator);
// Send the version negotiation message.
this.send(versionMessage);
};
// Prepare the version negotiation.
this.socket.once("connected", versionNegotiator);
// Set up handler for network errors.
this.socket.on("error", this.onSocketError.bind(this));
// Connect to the server.
this.socket.connect();
};
// Wait until connection is established and version negotiation succeeds.
await new Promise(connectionResolver);
}
/**
* Restores the network connection.
*/ async reconnect() {
// If a reconnect timer is set, remove it
await this.clearReconnectTimer();
// Write a log message.
(0, $dvphU$electrumcashdebuglogs).network(`Trying to reconnect to '${this.hostIdentifier}'..`);
// Set the status to reconnecting for more accurate log messages.
this.status = (0, $db7c797e63383364$export$7516420eb880ab68).RECONNECTING;
// Emit a connect event now that the connection is usable.
this.emit("reconnecting");
// Disconnect the underlying socket.
this.socket.disconnect();
try {
// Try to connect again.
await this.connect();
} catch (error) {
// Do nothing as the error should be handled via the disconnect and error signals.
}
}
/**
* Removes the current reconnect timer.
*/ clearReconnectTimer() {
// If a reconnect timer is set, remove it
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
// Reset the timer reference.
this.reconnectTimer = undefined;
}
/**
* Removes the current keep-alive timer.
*/ clearKeepAliveTimer() {
// If a keep-alive timer is set, remove it
if (this.keepAliveTimer) clearTimeout(this.keepAliveTimer);
// Reset the timer reference.
this.keepAliveTimer = undefined;
}
/**
* Initializes the keep alive timer loop.
*/ setupKeepAliveTimer() {
// If the keep-alive timer loop is not currently set up..
if (!this.keepAliveTimer) // Set a new keep-alive timer.
this.keepAliveTimer = setTimeout(this.ping.bind(this), this.options.sendKeepAliveIntervalInMilliSeconds);
}
/**
* Tears down the current connection and removes all event listeners on disconnect.
*
* @param force - disconnect even if the connection has not been fully established yet.
* @param intentional - update connection state if disconnect is intentional.
*
* @returns true if successfully disconnected, or false if there was no connection.
*/ async disconnect(force = false, intentional = true) {
// Return early when there is nothing to disconnect from
if (this.status === (0, $db7c797e63383364$export$7516420eb880ab68).DISCONNECTED && !force) // Return false to indicate that there was nothing to disconnect from.
return false;
// Update connection state if the disconnection is intentional.
// NOTE: The state is meant to represent what the client is requesting, but
// is used internally to handle visibility changes in browsers to ensure functional reconnection.
if (intentional) // Set connection status to null to indicate tear-down is currently happening.
this.status = (0, $db7c797e63383364$export$7516420eb880ab68).DISCONNECTING;
// Emit a connect event to indicate that we are disconnecting.
this.emit("disconnecting");
// If a keep-alive timer is set, remove it.
await this.clearKeepAliveTimer();
// If a reconnect timer is set, remove it
await this.clearReconnectTimer();
const disconnectResolver = (resolve)=>{
// Resolve to true after the connection emits a disconnect
this.once("disconnected", ()=>resolve(true));
// Close the connection on the socket level.
this.socket.disconnect();
};
// Return true to indicate that we disconnected.
return new Promise(disconnectResolver);
}
/**
* Updates the connection state based on browser reported connectivity.
*
* Most modern browsers are able to provide information on the connection state
* which allows for significantly faster response times to network changes compared
* to waiting for network requests to fail.
*
* When available, we make use of this to fail early to provide a better user experience.
*/ async handleNetworkChange() {
// Do nothing if we do not have the navigator available.
if (typeof window.navigator === "undefined") return;
// Attempt to reconnect to the network now that we may be online again.
if (window.navigator.onLine === true) this.reconnect();
// Disconnected from the network so that cleanup can happen while we're offline.
if (window.navigator.onLine !== true) {
const forceDisconnect = true;
const isIntentional = true;
this.disconnect(forceDisconnect, isIntentional);
}
}
/**
* Updates connection state based on application visibility.
*
* Some browsers will disconnect network connections when the browser is out of focus,
* which would normally cause our reconnect-on-timeout routines to trigger, but that
* results in a poor user experience since the events are not handled consistently
* and sometimes it can take some time after restoring focus to the browser.
*
* By manually disconnecting when this happens we prevent the default reconnection routines
* and make the behavior consistent across browsers.
*/ async handleVisibilityChange() {
// Disconnect when application is removed from focus.
if (document.visibilityState === "hidden") {
const forceDisconnect = true;
const isIntentional = true;
this.disconnect(forceDisconnect, isIntentional);
}
// Reconnect when application is returned to focus.
if (document.visibilityState === "visible") this.reconnect();
}
/**
* Sends an arbitrary message to the server.
*
* @param message - json encoded request object to send to the server, as a string.
*
* @returns true if the message was fully flushed to the socket, false if part of the message
* is queued in the user memory
*/ send(message) {
// Remove the current keep-alive timer if it exists.
this.clearKeepAliveTimer();
// Get the current timestamp in milliseconds.
const currentTime = Date.now();
// Follow up and verify that the message got sent..
const verificationTimer = setTimeout(this.verifySend.bind(this, currentTime), this.socket.timeout);
// Store the verification timer locally so that it can be cleared when data has been received.
this.verifications.push(verificationTimer);
// Set a new keep-alive timer.
this.setupKeepAliveTimer();
// Write the message to the network socket.
return this.socket.write(message + (0, $24139611f53a54b8$export$5d955335434540c6).statementDelimiter);
}
// --- Event managers. --- //
/**
* Marks the connection as timed out and schedules reconnection if we have not
* received data within the expected time frame.
*/ verifySend(sentTimestamp) {
// If we haven't received any data since we last sent data out..
if (Number(this.lastReceivedTimestamp) < sentTimestamp) {
// If this connection is already disconnected, we do not change anything
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}'`);
return;
// Remove the current keep-alive timer if it exists.
this.clearKeepAliveTimer();
// Write a notification to the logs.
(0, $dvphU$electrumcashdebuglogs).network(`Connection to '${this.hostIdentifier}' timed out.`);
// Close the connection to avoid re-use.
// NOTE: This initiates reconnection routines if the connection has not
// been marked as intentionally disconnected.
this.socket.disconnect();
}
}
/**
* Updates the connection status when a connection is confirmed.
*/ onSocketConnect() {
// If a reconnect timer is set, remove it.
this.clearReconnectTimer();
// Set up the initial timestamp for when we last received data from the server.
this.lastReceivedTimestamp = Date.now();
// Set up the initial keep-alive timer.
this.setupKeepAliveTimer();
// Clear all temporary error listeners.
this.socket.removeAllListeners("error");
// Set up handler for network errors.
this.socket.on("error", this.onSocketError.bind(this));
}
/**
* Updates the connection status when a connection is ended.
*/ onSocketDisconnect() {
// Remove the current keep-alive timer if it exists.
this.clearKeepAliveTimer();
// If this is a connection we're trying to tear down..
if (this.status === (0, $db7c797e63383364$export$7516420eb880ab68).DISCONNECTING) {
// Mark the connection as disconnected.
this.status = (0, $db7c797e63383364$export$7516420eb880ab68).DISCONNECTED;
// Send a disconnect signal higher up the stack.
this.emit("disconnected");
// If a reconnect timer is set, remove it.
this.clearReconnectTimer();
// Remove all event listeners
this.removeAllListeners();
// Write a log message.
(0, $dvphU$electrumcashdebuglogs).network(`Disconnected from '${this.hostIdentifier}'.`);
} else {
// If this is for an established connection..
if (this.status === (0, $db7c797e63383364$export$7516420eb880ab68).CONNECTED) // Write a notification to the logs.
(0, $dvphU$electrumcashdebuglogs).errors(`Connection with '${this.hostIdentifier}' was closed, trying to reconnect in ${this.options.reconnectAfterMilliSeconds / 1000} seconds.`);
// Mark the connection as disconnected for now..
this.status = (0, $db7c797e63383364$export$7516420eb880ab68).DISCONNECTED;
// Send a disconnect signal higher up the stack.
this.emit("disconnected");
// If we don't have a pending reconnection timer..
if (!this.reconnectTimer) // Attempt to reconnect after one keep-alive duration.
this.reconnectTimer = setTimeout(this.reconnect.bind(this), this.options.reconnectAfterMilliSeconds);
}
}
/**
* Notify administrator of any unexpected errors.
*/ onSocketError(error) {
// Report a generic error if no error information is present.
// NOTE: When using WSS, the error event explicitly
// only allows to send a "simple" event without data.
// https://stackoverflow.com/a/18804298
if (typeof error === "undefined") // Do nothing, and instead rely on the socket disconnect event for further information.
return;
// Log the error, as there is nothing we can do to actually handle it.
(0, $dvphU$electrumcashdebuglogs).errors(`Network error ('${this.hostIdentifier}'): `, error);
}
}
// Define number of milliseconds per second for legibility.
const $d801b1f9b7fc3074$var$MILLI_SECONDS_PER_SECOND = 1000;
const $d801b1f9b7fc3074$export$5ba3a4134d0d751d = {
// By default, all numbers including integers are parsed as regular JavaScript numbers.
useBigInt: false,
// Send a ping message every seconds, to detect network problem as early as possible.
sendKeepAliveIntervalInMilliSeconds: 1 * $d801b1f9b7fc3074$var$MILLI_SECONDS_PER_SECOND,
// Try to reconnect 5 seconds after unintentional disconnects.
reconnectAfterMilliSeconds: 5 * $d801b1f9b7fc3074$var$MILLI_SECONDS_PER_SECOND,
// Try to detect stale connections 5 seconds after every send.
verifyConnectionTimeoutInMilliSeconds: 5 * $d801b1f9b7fc3074$var$MILLI_SECONDS_PER_SECOND,
// Automatically manage the connection for a consistent behavior across browsers and devices.
disableBrowserVisibilityHandling: false,
disableBrowserConnectivityHandling: false
};
/**
* High-level Electrum client that lets applications send requests and subscribe to notification events from a server.
*/ class $558b46d3f899ced5$var$ElectrumClient extends (0, $dvphU$EventEmitter) {
/**
* Number corresponding to the underlying connection status.
*/ get status() {
return this.connection.status;
}
/**
* Initializes an Electrum client.
*
* @param application - your application name, used to identify to the electrum host.
* @param version - protocol version to use with the host.
* @param socketOrHostname - pre-configured electrum socket or fully qualified domain name or IP number of the host
* @param options - ...
*
* @throws {Error} if `version` is not a valid version string.
*/ constructor(application, version, socketOrHostname, options = {}){
// Initialize the event emitter.
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)();
// Update default options with the provided values.
const networkOptions = {
...(0, $d801b1f9b7fc3074$export$5ba3a4134d0d751d),
...options
};
// Set up a connection to an electrum server.
this.connection = new (0, $ff134c9a9e1f7361$export$de0f57fc22079b5e)(application, version, socketOrHostname, networkOptions);
}
// Expose hostIdentifier from the connection.
get hostIdentifier() {
return this.connection.hostIdentifier;
}
// Expose port from the connection.
get encrypted() {
return this.connection.encrypted;
}
/**
* Connects to the remote server.
*
* @throws {Error} if the socket connection fails.
* @returns a promise resolving when the connection is established.
*/ async connect() {
// Create a lock so that multiple connects/disconnects cannot race each other.
const unlock = await this.connectionLock.acquire();
try {
// If we are already connected, do not attempt to connect again.
if (this.connection.status === (0, $db7c797e63383364$export$7516420eb880ab68).CONNECTED) return;
// Listen for parsed statements.
this.connection.on("response", this.response.bind(this));
// Hook up handles for the connected and disconnected events.
this.connection.on("connected", this.resubscribeOnConnect.bind(this));
this.connection.on("disconnected", this.onConnectionDisconnect.bind(this));
// Relay connecting and reconnecting events.
this.connection.on("connecting", this.handleConnectionStatusChanges.bind(this, "connecting"));
this.connection.on("disconnecting", this.handleConnectionStatusChanges.bind(this, "disconnecting"));
this.connection.on("reconnecting", this.handleConnectionStatusChanges.bind(this, "reconnecting"));
// Hook up client metadata gathering functions.
this.connection.on("version", this.storeSoftwareVersion.bind(this));
this.connection.on("received", this.updateLastReceivedTimestamp.bind(this));
// Relay error events.
this.connection.on("error", this.emit.bind(this, "error"));
// Connect with the server.
await this.connection.connect();
} finally{
unlock();
}
}
/**
* Disconnects from the remote server and removes all event listeners/subscriptions and open requests.
*
* @param force - disconnect even if the connection has not been fully established yet.
* @param retainSubscriptions - retain subscription data so they will be restored on reconnection.
*
* @returns true if successfully disconnected, or false if there was no connection.
*/ async disconnect(force = false, retainSubscriptions = false) {
if (!retainSubscriptions) {
// Cancel all event listeners.
this.removeAllListeners();
// Remove all subscription data
this.subscriptionMethods = {};
}
// Disconnect from the remote server.
return this.connection.disconnect(force);
}
/**
* Calls a method on the remote server with the supplied parameters.
*
* @param method - name of the method to call.
* @param parameters - one or more parameters for the method.
*
* @throws {Error} if the client is disconnected.
* @returns a promise that resolves with the result of the method or an Error.
*/ async request(method, ...parameters) {
// If we are not connected to a server..
if (this.connection.status !== (0, $db7c797e63383364$export$7516420eb880ab68).CONNECTED) // Reject the request with a disconnected error message.
throw new Error(`Unable to send request to a disconnected server '${this.hostIdentifier}'.`);
// Increase the request ID by one.
this.requestId += 1;
// Store a copy of the request id.
const id = this.requestId;
// Format the arguments as an electrum request object.
const message = (0, $24139611f53a54b8$export$5d955335434540c6).buildRequestObject(method, parameters, id);
// Define a function to wrap the request in a promise.
const requestResolver = (resolve)=>{
// Add a request resolver for this promise to the list of requests.
this.requestResolvers[id] = (error, data)=>{
// If the resolution failed..
if (error) // Resolve the promise with the error for the application to handle.
resolve(error);
else // Resolve the promise with the request results.
resolve(data);
};
// Send the request message to the remote server.
this.connection.send(message);
};
// Write a log message.
(0, $dvphU$electrumcashdebuglogs).network(`Sending request '${method}' to '${this.hostIdentifier}'`);
// return a promise to deliver results later.
return new Promise(requestResolver);
}
/**
* Subscribes to the method and payload at the server.
*
* @remarks the response for the subscription request is issued as a notification event.
*
* @param method - one of the subscribable methods the server supports.
* @param parameters - one or more parameters for the method.
*
* @throws {Error} if the client is disconnected.
* @returns a promise resolving when the subscription is established.
*/ async subscribe(method, ...parameters) {
// Initialize an empty list of subscription payloads, if needed.
if (!this.subscriptionMethods[method]) this.subscriptionMethods[method] = new Set();
// Store the subscription parameters to track what data we have subscribed to.
this.subscriptionMethods[method].add(JSON.stringify(parameters));
// Send initial subscription request.
const requestData = await this.request(method, ...parameters);
// If the request failed, throw it as an error.
if (requestData instanceof Error) throw requestData;
// If the request returned more than one data point..
if (Array.isArray(requestData)) // .. throw an error, as this breaks our expectation for subscriptions.
throw new Error("Subscription request returned an more than one data point.");
// Construct a notification structure to package the initial result as a notification.
const notification = {
jsonrpc: "2.0",
method: method,
params: [
...parameters,
requestData
]
};
// Manually emit an event for the initial response.
this.emit("notification", notification);
// Try to update the chain height.
this.updateChainHeightFromHeadersNotifications(notification);
}
/**
* Unsubscribes to the method at the server and removes any callback functions
* when there are no more subscriptions for the method.
*
* @param method - a previously subscribed to method.
* @param parameters - one or more parameters for the method.
*
* @throws {Error} if no subscriptions exist for the combination of the provided `method` and `parameters.
* @throws {Error} if the client is disconnected.
* @returns a promise resolving when the subscription is removed.
*/ async unsubscribe(method, ...parameters) {
// Throw an error if the client is disconnected.
if (this.connection.status !== (0, $db7c797e63383364$export$7516420eb880ab68).CONNECTED) throw new Error(`Unable to send unsubscribe request to a disconnected server '${this.hostIdentifier}'.`);
// If this method has no subscriptions..
if (!this.subscriptionMethods[method]) // Reject this promise with an explanation.
throw new Error(`Cannot unsubscribe from '${method}' since the method has no subscriptions.`);
// Pack up the parameters as a long string.
const subscriptionParameters = JSON.stringify(parameters);
// If the method payload could not be located..
if (!this.subscriptionMethods[method].has(subscriptionParameters)) // Reject this promise with an explanation.
throw new Error(`Cannot unsubscribe from '${method}' since it has no subscription with the given parameters.`);
// Remove this specific subscription payload from internal tracking.
this.subscriptionMethods[method].delete(subscriptionParameters);
// Send unsubscription request to the server
// NOTE: As a convenience we allow users to define the method as the subscribe or unsubscribe version.
await this.request(method.replace(".subscribe", ".unsubscribe"), ...parameters);
// Write a log message.
(0, $dvphU$electrumcashdebuglogs).client(`Unsubscribed from '${String(method)}' for the '${subscriptionParameters}' parameters.`);
}
/**
* Restores existing subscriptions without updating status or triggering manual callbacks.
*
* @throws {Error} if subscription data cannot be found for all stored event names.
* @throws {Error} if the client is disconnected.
* @returns a promise resolving to true when the subscriptions are restored.
*
* @ignore
*/ async resubscribeOnConnect() {
// Write a log message.
(0, $dvphU$electrumcashdebuglogs).client(`Connected to '${this.hostIdentifier}'.`);
// Synchronize with the underlying connection status.
this.handleConnectionStatusChanges("connected");
// Initialize an empty list of resubscription promises.
const resubscriptionPromises = [];
// For each method we have a subscription for..
for(const method in this.subscriptionMethods){
// .. and for each parameter we have previously been subscribed to..
for (const parameterJSON of this.subscriptionMethods[method].values()){
// restore the parameters from JSON.
const parameters = JSON.parse(parameterJSON);
// Send a subscription request.
resubscriptionPromises.push(this.subscribe(method, ...parameters));
}
// Wait for all re-subscriptions to complete.
await Promise.all(resubscriptionPromises);
}
// Write a log message if there was any subscriptions to restore.
if (resubscriptionPromises.length > 0) (0, $dvphU$electrumcashdebuglogs).client(`Restored ${resubscriptionPromises.length} previous subscriptions for '${this.hostIdentifier}'`);
}
/**
* Parser messages from the remote server to resolve request promises and emit subscription events.
*
* @param message - the response message
*
* @throws {Error} if the message ID does not match an existing request.
* @ignore
*/ response(message) {
// If the received message is a notification, we forward it to all event listeners
if ((0, $abcb763a48577a1e$export$280de919a0cf6928)(message)) {
// Write a log message.
(0, $dvphU$electrumcashdebuglogs).client(`Received notification for '${message.method}' from '${this.hostIdentifier}'`);
// Forward the message content to all event listeners.
this.emit("notification", message);
// Try to update the chain height.
this.updateChainHeightFromHeadersNotifications(message);
// Return since it does not have an associated request resolver
return;
}
// If the response ID is null we cannot use it to index our request resolvers
if (message.id === null) // Throw an internal error, this should not happen.
throw new Error("Internal error: Received an RPC response with ID null.");
// Look up which request promise we should resolve this.
const requestResolver = this.requestResolvers[message.id];
// If we do not have a request resolver for this response message..
if (!requestResolver) {
// Log that a message was ignored since the request has already been rejected.
(0, $dvphU$electrumcashdebuglogs).warning(`Ignoring response #${message.id} as the request has already been rejected.`);
// Return as this has now been fully handled.
return;
}
// Remove the promise from the request list.
delete this.requestResolvers[message.id];
// If the message contains an error..
if ((0, $abcb763a48577a1e$export$d73a2e87a509880)(message)) // Forward the message error to the request resolver and omit the `result` parameter.
requestResolver(new Error(message.error.message));
else {
// Forward the message content to the request resolver and omit the `error` parameter
// (by setting it to undefined).
requestResolver(undefined, message.result);
// Attempt to extract genesis hash from feature requests.
this.storeGenesisHashFromFeaturesResponse(message);
}
}
/**
* Callback function that is called when connection to the Electrum server is lost.
* Aborts all active requests with an error message indicating that connection was lost.
*
* @ignore
*/ async onConnectionDisconnect() {
// Loop over active requests
for(const resolverId in this.requestResolvers){
// Extract request resolver for readability
const requestResolver = this.requestResolvers[resolverId];
// Resolve the active request with an error indicating that the connection was lost.
requestResolver(new Error("Connection lost"));
// Remove the promise from the request list.
delete this.requestResolvers[resolverId];
}
// Synchronize with the underlying connection status.
this.handleConnectionStatusChanges("disconnected");
}
/**
* Stores the server provider software version field on successful version negotiation.
*
* @ignore
*/ async storeSoftwareVersion(versionStatement) {
// TODO: handle failed version negotiation better.
if (versionStatement.error) // Do nothing.
return;
// Store the software version.
this.software = versionStatement.software;
}
/**
* Updates the last received timestamp.
*
* @ignore
*/ async updateLastReceivedTimestamp() {
// Update the timestamp for when we last received data.
this.lastReceivedTimestamp = Date.now();
}
/**
* Checks if the provided message is a response to a headers subscription,
* and if so updates the locally stored chain height value for this client.
*
* @ignore
*/ async updateChainHeightFromHeadersNotifications(message) {
// If the message is a notification for a new chain height..
if (message.method === "blockchain.headers.subscribe") // ..also store the updated chain height locally.
this.chainHeight = message.params[0].height;
}
/**
* Checks if the provided message is a response to a server.features request,
* and if so stores the genesis hash for this client locally.
*
* @ignore
*/ async storeGenesisHashFromFeaturesResponse(message) {
try {
// If the message is a response to a features request..
if (typeof message.result.genesis_hash !== "undefined") // ..store the genesis hash locally.
this.genesisHash = message.result.genesis_hash;
} catch (error) {
// Do nothing.
}
}
/**
* Helper function to synchronize state and events with the underlying connection.
*/ async handleConnectionStatusChanges(eventName) {
// Re-emit the event.
this.emit(eventName);
}
}
var // Export the client.
$558b46d3f899ced5$export$2e2bcd8739ae039 = $558b46d3f899ced5$var$ElectrumClient;
export {$558b46d3f899ced5$export$2e2bcd8739ae039 as ElectrumClient, $e83d2e7688025acd$export$e1f38ab2b4ebdde6 as isVersionRejected, $e83d2e7688025acd$export$9598f0c76aa41d73 as isVersionNegotiated, $db7c797e63383364$export$7516420eb880ab68 as ConnectionStatus};
//# sourceMappingURL=index.mjs.map