UNPKG

@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
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