// Use strict "use strict"; // Classes // Node class class Node { // Public // Constructor constructor(torProxy, settings) { // Set Tor proxy this.torProxy = torProxy; // Set settings this.settings = settings; // Set retry delay to initial value this.retryDelay = Node.INITIAL_RETRY_DELAY_MILLISECONDS; // Set reconnect timeout to no timeout this.reconnectTimeout = Node.NO_TIMEOUT; // Set started to false this.started = false; // Set connected to fase this.connected = false; // Set failed to connect this.failedToConnect = false; // Set last height response timestamp this.lastHeightResponseTimestamp = Node.NO_HEIGHT_RESPONSE_TIMESTAMP; // Set update height timeout to no timeout this.updateHeightTimeout = Node.NO_TIMEOUT; // Set current height to a height with an unknown height this.currentHeight = new Height(Node.UNKNOWN_HEIGHT); // Set version obtained to false this.versionObtained = false; // Set use custom node to setting's default value this.useCustomNode = Node.SETTINGS_USE_CUSTOM_NODE_DEFAULT_VALUE; // Set custom node address to setting's default value this.customNodeAddress = Node.SETTINGS_CUSTOM_NODE_ADDRESS_DEFAULT_VALUE; // Set custom node secret to setting's default value this.customNodeSecret = Node.SETTINGS_CUSTOM_NODE_SECRET_DEFAULT_VALUE; // Set empty address this.emptyAddress = true; // Set cached PMMR indices response to no cached response this.cachedPmmrIndicesResponse = Node.NO_CACHED_RESPONSE; // Set cache PMMR indicies parameters to no cache parameters this.cachePmmrIndiciesParameters = Node.NO_CACHE_PARAMETERS; // Set cached unspent outputs response to no cached response this.cachedUnspentOutputsResponse = Node.NO_CACHED_RESPONSE; // Set cache unspent outputs parameters to no cache parameters this.cacheUnspentOutputsParameters = Node.NO_CACHE_PARAMETERS; // Set cached header responses this.cachedHeaderResponses = {}; // Set cached kernel responses this.cachedKernelResponses = {}; // Set ignore response index this.ignoreResponseIndex = 0; // Set self var self = this; // Once database is initialized Database.onceInitialized(function() { // Return promise return new Promise(function(resolve, reject) { // Return creating settings return Promise.all([ // Use custom node setting self.settings.createValue(Node.SETTINGS_USE_CUSTOM_NODE_NAME, Node.SETTINGS_USE_CUSTOM_NODE_DEFAULT_VALUE), // Custom node address setting self.settings.createValue(Node.SETTINGS_CUSTOM_NODE_ADDRESS_NAME, Node.SETTINGS_CUSTOM_NODE_ADDRESS_DEFAULT_VALUE), // Custom node secret setting self.settings.createValue(Node.SETTINGS_CUSTOM_NODE_SECRET_NAME, Node.SETTINGS_CUSTOM_NODE_SECRET_DEFAULT_VALUE) ]).then(function() { // Initialize settings var settings = [ // Use custom node setting Node.SETTINGS_USE_CUSTOM_NODE_NAME, // Custom node address setting Node.SETTINGS_CUSTOM_NODE_ADDRESS_NAME, // Custom node secret setting Node.SETTINGS_CUSTOM_NODE_SECRET_NAME ]; // Return getting settings' values return Promise.all(settings.map(function(setting) { // Return getting setting's value return self.settings.getValue(setting); })).then(function(settingValues) { // Set use custom node to setting's value self.useCustomNode = settingValues[settings.indexOf(Node.SETTINGS_USE_CUSTOM_NODE_NAME)]; // Set custom node address to setting's value self.customNodeAddress = settingValues[settings.indexOf(Node.SETTINGS_CUSTOM_NODE_ADDRESS_NAME)]; // Set custom node secret to setting's value self.customNodeSecret = settingValues[settings.indexOf(Node.SETTINGS_CUSTOM_NODE_SECRET_NAME)]; // Resolve resolve(); // Catch errors }).catch(function(error) { // Reject reject(); }); // Catch errors }).catch(function(error) { // Reject reject(); }); }); }); // Settings change event $(this.settings).on(Settings.CHANGE_EVENT, function(event, setting) { // Initialize node setting changed var nodeSettingChanged = false; // Check what setting was changes switch(setting[Settings.DATABASE_SETTING_NAME]) { // Use custom node setting case Node.SETTINGS_USE_CUSTOM_NODE_NAME: // Set use custom node to setting's value self.useCustomNode = setting[Settings.DATABASE_VALUE_NAME]; // Set node setting changed nodeSettingChanged = true; // Break break; // Custom node address setting case Node.SETTINGS_CUSTOM_NODE_ADDRESS_NAME: // Set custom node address to setting's value self.customNodeAddress = setting[Settings.DATABASE_VALUE_NAME]; // Set node setting changed nodeSettingChanged = true; // Break break; // Custom node secret setting case Node.SETTINGS_CUSTOM_NODE_SECRET_NAME: // Set custom node secret to setting's value self.customNodeSecret = setting[Settings.DATABASE_VALUE_NAME]; // Set node setting changed nodeSettingChanged = true; // Break break; } // Check if a node setting was changed if(nodeSettingChanged === true) { // Check if connected to the node if(self.versionObtained === true) { // Log message Log.logMessage(Language.getDefaultTranslation('Node settings changed. Disconnecting from the node.')); } // Clear version obtained self.versionObtained = false; // Trigger settings change event $(self).trigger(Node.SETTINGS_CHANGE_EVENT); // Restart self.restart(); } }); // Tor proxy settings change event $(this.torProxy).on(TorProxy.SETTINGS_CHANGE_EVENT, function() { // Check if using a custom node, the custom node exists, and not using a custom Tor proxy or a Tor proxy address exists if(self.usingCustomNode() === true && self.getAddresses(Consensus.getNetworkType() === Consensus.MAINNET_NETWORK_TYPE)[0]["length"] !== 0 && (self.torProxy.usingCustomTorProxy() === false || self.torProxy.getAddress()["length"] !== 0)) { // Get URL as the first address var url = self.getAddresses(Consensus.getNetworkType() === Consensus.MAINNET_NETWORK_TYPE)[0]; // Check if Tor proxy will be used to connect to the node if(Tor.isTorUrl(url) === true && Tor.isSupported() === false) { // Check if connected if(self.isConnected() === true) { // Update height immediately self.updateHeight(true); } // Otherwise else { // Clear version obtained self.versionObtained = false; // Restart self.restart(); } } } }); // Connection warning or close event $(this).on(Node.CONNECTION_WARNING_EVENT + " " + Node.CONNECTION_CLOSE_EVENT, function(event, type) { // Check type switch(type) { // No connection close type case Node.NO_CONNECTION_CLOSE_TYPE: // Check if address exists if(self.emptyAddress === false) { // Log message Log.logMessage(Language.getDefaultTranslation('Connecting to the node failed.')); } // Break break; // Disconnected close type case Node.DISCONNECTED_CLOSE_TYPE: // Log message Log.logMessage(Language.getDefaultTranslation('Disconnected from the node.')); // Break break; // Unauthorized warning type case Node.UNAUTHORIZED_WARNING_TYPE: // Log message Log.logMessage(Language.getDefaultTranslation('You\'re not authorized to connect to the node.')); // Break break; // Invalid response warning type case Node.INVALID_RESPONSE_WARNING_TYPE: // Log message Log.logMessage(Language.getDefaultTranslation('Received an invalid response from the node.')); // Break break; // Incompatible warning type case Node.INCOMPATIBLE_WARNING_TYPE: // Log message Log.logMessage(Language.getDefaultTranslation('Not compatible with node versions less than %1$v.'), [ // Version Node.MINIMUM_COMPATIBLE_NODE_VERSION ]); // Break break; } // Check if reconnect timeout exists if(self.reconnectTimeout !== Node.NO_TIMEOUT) { // Clear reconnect timeout clearTimeout(self.reconnectTimeout); // Set reconnect timeout to no timeout self.reconnectTimeout = Node.NO_TIMEOUT; } // Set reconnect timeout self.reconnectTimeout = setTimeout(function() { // Set reconnect timeout to no timeout self.reconnectTimeout = Node.NO_TIMEOUT; // Connect self.connect(); }, Math.min(Node.MAXIMUM_RETRY_DELAY_MILLISECONDS, self.retryDelay)); // Update retry delay self.retryDelay *= Node.RETRY_DELAY_SCALING_FACTOR; }); // Window online event $(window).on("online", function() { // Check if reconnect timeout exists if(self.reconnectTimeout !== Node.NO_TIMEOUT) { // Clear reconnect timeout clearTimeout(self.reconnectTimeout); // Set reconnect timeout to no timeout self.reconnectTimeout = Node.NO_TIMEOUT; // Reset retry delay self.retryDelay = Node.INITIAL_RETRY_DELAY_MILLISECONDS; // Connect self.connect(); } // Window offline event }).on("offline", function() { // Check if started if(self.started === true) { // Reset self.reset(); // Trigger connection close event $(self).trigger(Node.CONNECTION_CLOSE_EVENT, (self.versionObtained === false) ? Node.NO_CONNECTION_CLOSE_TYPE : Node.DISCONNECTED_CLOSE_TYPE); // Clear version obtained self.versionObtained = false; } // Window focus event }).on("focus", function() { // Check if connected and hasn't received a height response recently if(self.isConnected() === true && self.lastHeightResponseTimestamp !== Node.NO_HEIGHT_RESPONSE_TIMESTAMP && self.lastHeightResponseTimestamp < Common.getCurrentTimestamp() - Node.MAXIMUM_NO_HEIGHT_RESPONSE_DURATION_SECONDS) // Update height immediately self.updateHeight(true); }); } // Start start() { // Check if not already started if(this.started === false) { // Set started this.started = true; // Connect this.connect(); } } // Restart restart(disconnectFirst = true) { // Check if not already started if(this.started === false) // Start this.start(); // Otherwise else { // Check if update height timeout exists if(this.updateHeightTimeout !== Node.NO_TIMEOUT) { // Clear update height timeout clearTimeout(this.updateHeightTimeout); // Set update height timeout to no timeout this.updateHeightTimeout = Node.NO_TIMEOUT; } // Check if disconnecting first if(disconnectFirst === true) { // Clear connected this.connected = false; // Clear failed to connect this.failedToConnect = false; // Clear last height response timestamp this.lastHeightResponseTimestamp = Node.NO_HEIGHT_RESPONSE_TIMESTAMP; // Set current height's height to an unknown height this.currentHeight.setHeight(Node.UNKNOWN_HEIGHT); } // Check if reconnect timeout exists if(this.reconnectTimeout !== Node.NO_TIMEOUT) { // Clear reconnect timeout clearTimeout(this.reconnectTimeout); // Set reconnect timeout to no timeout this.reconnectTimeout = Node.NO_TIMEOUT; } // Reset retry delay this.retryDelay = Node.INITIAL_RETRY_DELAY_MILLISECONDS; // Connect this.connect(disconnectFirst); } } // Using custom node usingCustomNode() { // Return if using a custom node return this.useCustomNode === true; } // Get addresses getAddresses(isMainnet) { // Check if not using a custom node if(this.usingCustomNode() === false) { // Check wallet type switch(Consensus.getWalletType()) { // MWC wallet case Consensus.MWC_WALLET_TYPE: // Check if mainnet if(isMainnet === true) { // Return addresses return [ // Addresses "https://mwc713.mwc.mw", "https://mwc7132.mwc.mw", "https://mwc7133.mwc.mw", "https://mwc7134.mwc.mw", "https://mwc7135.mwc.mw", "https://mwc7136.mwc.mw" ]; } // Otherwise else { // Return addresses return [ // Addresses "https://mwc713.floonet.mwc.mw" ]; } // Break break; // GRIN wallet case Consensus.GRIN_WALLET_TYPE: // Check if mainnet if(isMainnet === true) { // Return addresses return [ // Addresses "https://grinnode.live:3413" ]; } // Otherwise else { // Return addresses return [ // Addresses "http://localhost:13413" ]; } // Break break; // EPIC wallet case Consensus.EPIC_WALLET_TYPE: // Check if mainnet if(isMainnet === true) { // Return addresses return [ // Addresses "https://node.epiccash.com:3413" ]; } // Otherwise else { // Return addresses return [ // Addresses "http://localhost:13413" ]; } // Break break; } } // Otherwise else { // Get custom node address var customNodeAddress = this.customNodeAddress.trim(); // Check if custom node address isn't set if(customNodeAddress["length"] === 0) { // Return return [ // Custom node address customNodeAddress ]; } // Otherwise else { // Check if custom node address doesn't have a protocol if(Common.urlContainsProtocol(customNodeAddress) === false) { // Add protocol to custom node address customNodeAddress = Common.HTTP_PROTOCOL + "//" + customNodeAddress; } // Return return [ // Custom node address upgraded if applicable and without any trailing slashes Common.removeTrailingSlashes(Common.upgradeApplicableInsecureUrl(customNodeAddress)) ]; } } } // Is connected isConnected() { // Return if connected return this.connected === true; } // Connection failed connectionFailed() { // Return if failed to connect return this.failedToConnect === true; } // Get current height getCurrentHeight() { // Return a copy of current height return new Height(this.currentHeight.getHeight(), this.currentHeight.getHash()); } // Get PMMR indices getPmmrIndices(startBlockHeight, endBlockHeight) { // Check if response is cached if(this.cachedPmmrIndicesResponse !== Node.NO_CACHED_RESPONSE && this.cachePmmrIndiciesParameters !== Node.NO_CACHE_PARAMETERS && startBlockHeight.isEqualTo(this.cachePmmrIndiciesParameters[0]) === true && endBlockHeight.isEqualTo(this.cachePmmrIndiciesParameters[1]) === true) { // Set self var self = this; // Return new promise return new Promise(function(resolve, reject) { // Check if cache is still valid if(self.cachedPmmrIndicesResponse !== Node.NO_CACHED_RESPONSE) { // Wait for cached PMMR indices response return self.cachedPmmrIndicesResponse.then(function(response) { // Check if cache is still valid if(self.cachedPmmrIndicesResponse !== Node.NO_CACHED_RESPONSE) { // Resolve response resolve(response); } // Otherwise else { // Reject reject(); } // Catch errors }).catch(function(error) { // Reject error reject(error); }); } // Otherwise else { // Reject reject(); } }); } // Otherwise else { // Set cache PMMR indicies parameters this.cachePmmrIndiciesParameters = [ // Start block height startBlockHeight, // End block height endBlockHeight ]; // Set self var self = this; // Set cached PMMR indices response this.cachedPmmrIndicesResponse = new Promise(function(resolve, reject) { // Return getting PMMR indices from response return self.getResponse("get_pmmr_indices", Node.OBJECT_RESPONSE_TYPE, [ // Start block height startBlockHeight, // End block height endBlockHeight ], [ // Last retrieved index "last_retrieved_index", // Highest index "highest_index" ], [ // Last retrieved index validation function(lastRetrievedIndex) { // Return if last retrieved index is valid return (Common.isNumberString(lastRetrievedIndex) === true || lastRetrievedIndex instanceof BigNumber === true) && (new BigNumber(lastRetrievedIndex)).isInteger() === true && (new BigNumber(lastRetrievedIndex)).isPositive() === true; }, // Highest index validation function(highestIndex) { // Return if highest index is valid return (Common.isNumberString(highestIndex) === true || highestIndex instanceof BigNumber === true) && (new BigNumber(highestIndex)).isInteger() === true && (new BigNumber(highestIndex)).isPositive() === true; } ]).then(function(value) { // Resolve resolve({ // Last retrieved index "last_retrieved_index": new BigNumber(value["last_retrieved_index"]), // Highest index "highest_index": new BigNumber(value["highest_index"]) }); // Catch errors }).catch(function(error) { // Reject error reject(error); }); }); // Return new promise return new Promise(function(resolve, reject) { // Check if getting cached PMMR indices is still valid if(self.cachedPmmrIndicesResponse !== Node.NO_CACHED_RESPONSE) { // Return cached PMMR indices response return self.cachedPmmrIndicesResponse.then(function(response) { // Resolve response resolve(response); // Catch errors }).catch(function(error) { // Set cached PMMR indices response to no cached response self.cachedPmmrIndicesResponse = Node.NO_CACHED_RESPONSE; // Set cache PMMR indicies parameters to no cache parameters self.cachePmmrIndiciesParameters = Node.NO_CACHE_PARAMETERS; // Reject error reject(error); }); } // Otherwise else { // Reject reject(); } }); } } // Get unspent outputs getUnspentOutputs(startIndex, endIndex, max, pmmrIndices = Node.NO_PMMR_INDICES) { // Check if start index is zero if(startIndex.isZero() === true) { // Set start index to first index startIndex = Node.FIRST_INDEX; } // Check if end index is zero if(endIndex.isZero() === true) { // Set end index to first index endIndex = Node.FIRST_INDEX; } // Check if response is cached if(this.cachedUnspentOutputsResponse !== Node.NO_CACHED_RESPONSE && this.cacheUnspentOutputsParameters !== Node.NO_CACHE_PARAMETERS && startIndex.isEqualTo(this.cacheUnspentOutputsParameters[0]) === true && endIndex.isEqualTo(this.cacheUnspentOutputsParameters[1]) === true && max.isEqualTo(this.cacheUnspentOutputsParameters[2]) === true) { // Set self var self = this; // Return new promise return new Promise(function(resolve, reject) { // Check if cache is still valid if(self.cachedUnspentOutputsResponse !== Node.NO_CACHED_RESPONSE) { // Wait for cached unspent outputs response return self.cachedUnspentOutputsResponse.then(function(response) { // Check if cache is still valid if(self.cachedUnspentOutputsResponse !== Node.NO_CACHED_RESPONSE) { // Check if PMMR indices are provided and current height is known if(pmmrIndices !== Node.NO_PMMR_INDICES && self.currentHeight.getHeight() !== Node.UNKNOWN_HEIGHT) { // Check if not at the last output if(response["highest_index"].isLessThanOrEqualTo(response["last_retrieved_index"]) === false) { // Get next PMMR indicies to populate the cache self.getPmmrIndices(response["outputs"][response["outputs"]["length"] - 1]["block_height"], self.currentHeight.getHeight()).then(function(newPmmrIndices) { // Check if start index to one after the last retrieved index if(newPmmrIndices["last_retrieved_index"].isEqualTo(pmmrIndices["last_retrieved_index"]) === true && response["last_retrieved_index"].isGreaterThanOrEqualTo(pmmrIndices["last_retrieved_index"]) === true) { // Get next unspent outputs to populate the cache self.getUnspentOutputs(response["last_retrieved_index"].plus(1), newPmmrIndices["highest_index"], max).catch(function(error) { }); } // Otherwise else { // Get next unspent outputs to populate the cache self.getUnspentOutputs(newPmmrIndices["last_retrieved_index"], newPmmrIndices["highest_index"], max).catch(function(error) { }); } // Catch errors }).catch(function(error) { }); } } // Resolve response resolve(response); } // Otherwise else { // Reject reject(); } // Catch errors }).catch(function(error) { // Reject error reject(error); }); } // Otherwise else { // Reject reject(); } }); } // Otherwise else { // Set cache unspent outputs parameters this.cacheUnspentOutputsParameters = [ // Start index startIndex, // End index endIndex, // Max max ]; // Set self var self = this; // Set cached unspent outputs response this.cachedUnspentOutputsResponse = new Promise(function(resolve, reject) { // Return getting unspent outputs from response return self.getResponse("get_unspent_outputs", Node.OBJECT_RESPONSE_TYPE, [ // Start index startIndex, // End index endIndex, // Max max, // Include proof true ], [ // Highest index "highest_index", // Last retrieved index "last_retrieved_index", // Outputs "outputs" ], [ // Highest index validation function(highestIndex) { // Return if highest index is valid return (Common.isNumberString(highestIndex) === true || highestIndex instanceof BigNumber === true) && (new BigNumber(highestIndex)).isInteger() === true && (new BigNumber(highestIndex)).isGreaterThanOrEqualTo(startIndex) === true; }, // Last retrieved index validation function(lastRetrievedIndex) { // Return if last retrieved index is valid return (Common.isNumberString(lastRetrievedIndex) === true || lastRetrievedIndex instanceof BigNumber === true) && (new BigNumber(lastRetrievedIndex)).isInteger() === true && (new BigNumber(lastRetrievedIndex)).isGreaterThanOrEqualTo(startIndex) === true; }, // Outputs validation function(outputs) { // Check if outputs isn't an array if(Array.isArray(outputs) === false) // Return false return false; // Go through all outputs for(var i = 0; i < outputs["length"]; ++i) { // Get output var output = outputs[i]; // Check if output is invalid if(Object.isObject(output) === false) // Return false return false; // Check if output's commit is invalid if("commit" in output === false || Common.isHexString(output["commit"]) === false || Common.hexStringLength(output["commit"]) !== Crypto.COMMIT_LENGTH) // Return false return false; // Check if output's proof is invalid if("proof" in output === false || Common.isHexString(output["proof"]) === false || Common.hexStringLength(output["proof"]) !== Crypto.PROOF_LENGTH) // Return false return false; // Check if output's output type is invalid if("output_type" in output === false || (output["output_type"] !== Output.COINBASE_TYPE && output["output_type"] !== Output.TRANSACTION_TYPE)) // Return false return false; // Check if output's block height is invalid if("block_height" in output === false || (Common.isNumberString(output["block_height"]) === false && output["block_height"] instanceof BigNumber === false) || (new BigNumber(output["block_height"])).isInteger() === false || (new BigNumber(output["block_height"])).isGreaterThanOrEqualTo(Consensus.FIRST_BLOCK_HEIGHT) === false) // Return false return false; } // Return true return true; } ]).then(function(value) { // Initialize result var result = { // Highest index "highest_index": new BigNumber(value["highest_index"]), // Last retrieved index "last_retrieved_index": new BigNumber(value["last_retrieved_index"]), // Outputs "outputs": value["outputs"].map(function(output) { // Return return { // Commit "commit": Common.fromHexString(output["commit"]), // Proof "proof": Common.fromHexString(output["proof"], true), // Output type "output_type": output["output_type"], // Block height "block_height": new BigNumber(output["block_height"]) }; }) }; // Check if PMMR indices are provided and current height is known if(pmmrIndices !== Node.NO_PMMR_INDICES && self.currentHeight.getHeight() !== Node.UNKNOWN_HEIGHT) { // Check if not at the last output if(result["highest_index"].isLessThanOrEqualTo(result["last_retrieved_index"]) === false) { // Get next PMMR indicies to populate the cache self.getPmmrIndices(result["outputs"][result["outputs"]["length"] - 1]["block_height"], self.currentHeight.getHeight()).then(function(newPmmrIndices) { // Check if start index to one after the last retrieved index if(newPmmrIndices["last_retrieved_index"].isEqualTo(pmmrIndices["last_retrieved_index"]) === true && result["last_retrieved_index"].isGreaterThanOrEqualTo(pmmrIndices["last_retrieved_index"]) === true) { // Get next unspent outputs to populate the cache self.getUnspentOutputs(result["last_retrieved_index"].plus(1), newPmmrIndices["highest_index"], max).catch(function(error) { }); } // Otherwise else { // Get next unspent outputs to populate the cache self.getUnspentOutputs(newPmmrIndices["last_retrieved_index"], newPmmrIndices["highest_index"], max).catch(function(error) { }); } // Catch errors }).catch(function(error) { }); } } // Resolve result resolve(result); // Catch errors }).catch(function(error) { // Reject error reject(error); }); }); // Return new promise return new Promise(function(resolve, reject) { // Check if getting cached unspent outputs is still valid if(self.cachedUnspentOutputsResponse !== Node.NO_CACHED_RESPONSE) { // Return cached unspent outputs response return self.cachedUnspentOutputsResponse.then(function(response) { // Resolve response resolve(response); // Catch errors }).catch(function(error) { // Set cached unspent outputs response to no cached response self.cachedUnspentOutputsResponse = Node.NO_CACHED_RESPONSE; // Set cache unspent outputs parameters to no cache parameters self.cacheUnspentOutputsParameters = Node.NO_CACHE_PARAMETERS; // Reject error reject(error); }); } // Otherwise else { // Reject reject(); } }); } } // Get outputs getOutputs(outputCommits) { // Set self var self = this; // Return promise return new Promise(function(resolve, reject) { // Initialize output commits index var outputCommitsIndex = 0; // Return getting outputs from response return self.getResponse("get_outputs", Node.ARRAY_RESPONSE_TYPE, [ // Output commit outputCommits.map(function(outputCommit) { // Return output commit as a hex string return Common.toHexString(outputCommit); }), // Start height Node.API_NO_VALUE, // End height Node.API_NO_VALUE, // Include proof true, // Include Merkle proof false ], [ // Commit "commit", // Block height "block_height", // Output type "output_type", // Proof "proof" ], [ // Commit validation function(commit, index) { // Return if commit is invalid if(Common.isHexString(commit) === false || Common.hexStringLength(commit) !== Crypto.COMMIT_LENGTH || index >= outputCommits["length"]) { // Return false return false; } // Loop while commit isn't for the output commit at the current index while(Common.arraysAreEqual(outputCommits[outputCommitsIndex], Common.fromHexString(commit)) === false) { // Increment output commits index and check if at the last output commit if(++outputCommitsIndex === outputCommits["length"]) { // Return false return false; } } // Increment output commits index ++outputCommitsIndex; // Return true return true; }, // Block height validation function(blockHeight, index) { // Return if block height is valid return (Common.isNumberString(blockHeight) === true || blockHeight instanceof BigNumber === true) && (new BigNumber(blockHeight)).isInteger() === true && (new BigNumber(blockHeight)).isGreaterThanOrEqualTo(Consensus.FIRST_BLOCK_HEIGHT) === true; }, // Output type validation function(outputType, index) { // Return if output type is valid return outputType === Output.COINBASE_TYPE || outputType === Output.TRANSACTION_TYPE; }, // Proof validation function(proof, index) { // Check if proof is valid return Common.isHexString(proof) === true && Common.hexStringLength(proof) === Crypto.PROOF_LENGTH; } ]).then(function(values) { // Initialize outputs var outputs = []; // Go through all requested outputs for(var i = 0, j = 0; i < outputCommits["length"]; ++i) { // Check if response exists if(j < values["length"]) { // Get output var output = values[j]; // Check if response is for the current requested output if(Common.arraysAreEqual(outputCommits[i], Common.fromHexString(output["commit"])) === true) { // Append output to list outputs.push({ // Commit "commit": Common.fromHexString(output["commit"]), // Proof "proof": Common.fromHexString(output["proof"], true), // Output type "output_type": output["output_type"], // Block height "block_height": new BigNumber(output["block_height"]) }); // Increment output index ++j; } // Otherwise else { // Append no output found to list outputs.push(Node.NO_OUTPUT_FOUND); } } // Otherwise else { // Append no output found to list outputs.push(Node.NO_OUTPUT_FOUND); } } // Resolve outputs resolve(outputs); // Catch errors }).catch(function(error) { // Reject error reject(error); }); }); } // Get header getHeader(headerHeight) { // Set self var self = this; // Return promise return new Promise(function(resolve, reject) { // Check if response is cached if(self.cachedHeaderResponses.hasOwnProperty(headerHeight.toFixed()) === true) { // Resolve cached response resolve(self.cachedHeaderResponses[headerHeight.toFixed()]); } // Otherwise else { // Return getting header from response return self.getResponse("get_header", Node.OBJECT_RESPONSE_TYPE, [ // Header height headerHeight, // Hash Node.API_NO_VALUE, // Commit Node.API_NO_VALUE ], [ // Height "height", // Hash "hash", // Timestamp "timestamp" ], [ // Height validation function(height) { // Return if height is valid return (Common.isNumberString(height) === true || height instanceof BigNumber === true) && (new BigNumber(height)).isEqualTo(headerHeight) === true; }, // Hash validation function(hash) { // Return if hash is valid return Common.isHexString(hash) === true && Common.hexStringLength(hash) === RecentHeights.HEADER_HASH_LENGTH; }, // Timestamp validation Common.isRfc3339String ], false, true).then(function(value) { // Initialize result var result = { // Hash "hash": Common.fromHexString(value["hash"]), // Timestamp "timestamp": Common.rfc3339StringToTimestamp(value["timestamp"]) }; // Cache result self.cachedHeaderResponses[headerHeight.toFixed()] = result; // Resolve result resolve(result); // Catch errors }).catch(function(error) { // Check if no header exists at the height if(Node.isNotFoundError(error) === true) { // Cache result self.cachedHeaderResponses[headerHeight.toFixed()] = Node.NO_HEADER_FOUND; // Resolve no header found resolve(Node.NO_HEADER_FOUND); } // Otherwise else // Reject error reject(error); }); } }); } // Get kernel getKernel(excess, minimumHeight, maximumHeight) { // Set self var self = this; // Return promise return new Promise(function(resolve, reject) { // Check if response is cached if(self.cachedKernelResponses.hasOwnProperty(Common.toHexString(excess)) === true) { // Resolve cached response resolve(self.cachedKernelResponses[Common.toHexString(excess)]); } // Otherwise else { // Return getting kernel from response return self.getResponse("get_kernel", Node.OBJECT_RESPONSE_TYPE, [ // Excess Common.toHexString(excess), // Minimum height minimumHeight, // Maximum height maximumHeight ], [ // Kernel "tx_kernel", // Height "height" ], [ // Kernel validation function(kernel) { // Check if kernel is invalid if(Object.isObject(kernel) === false) // Return false return false; // Check if kernel's excess is invalid if("excess" in kernel === false || Common.isHexString(kernel["excess"]) === false || Common.hexStringLength(kernel["excess"]) !== Crypto.COMMIT_LENGTH || Common.arraysAreEqual(excess, Common.fromHexString(kernel["excess"])) === false) // Return false return false; // Check if kernel's features is invalid if("features" in kernel === false || Object.isObject(kernel["features"]) === false || Object.keys(kernel["features"])["length"] !== SlateKernel.TRANSACTION_FEATURES_LENGTH || SlateKernel.textToFeatures(Object.keys(kernel["features"])[0]) === SlateKernel.UNSUPPORTED_FEATURES) // Return false return false; // Return true return true; }, // Height validation function(height) { // Return if height is valid return (Common.isNumberString(height) === true || height instanceof BigNumber === true) && (new BigNumber(height)).isInteger() === true && (new BigNumber(height)).isGreaterThanOrEqualTo(Consensus.FIRST_BLOCK_HEIGHT) === true; } ], false, true).then(function(value) { // Initialize result var result = { // Kernel "tx_kernel": { // Features "features": value["tx_kernel"]["features"] }, // Height "height": new BigNumber(value["height"]) }; // Cache result self.cachedKernelResponses[Common.toHexString(excess)] = result; // Resolve result resolve(result); // Catch errors }).catch(function(error) { // Check if no kernel exists in the height range if(Node.isNotFoundError(error) === true) { // Cache result self.cachedKernelResponses[Common.toHexString(excess)] = Node.NO_KERNEL_FOUND; // Resolve no kernel found resolve(Node.NO_KERNEL_FOUND); } // Otherwise else // Reject error reject(error); }); } }); } // Broadcast transaction broadcastTransaction(transaction) { // Set self var self = this; // Return promise return new Promise(function(resolve, reject) { // Return pushing transaction from response return self.getResponse("push_transaction", Node.NO_RESPONSE_TYPE, [ // Transaction transaction, // Fluff false, ], Node.NO_VALUE, [], false, false, true).then(function(value) { // Resolve value resolve(value); // Catch errors }).catch(function(error) { // Reject error reject(error); }); }); } // Clear cache clearCache() { // Set cached PMMR indices response to no cached response this.cachedPmmrIndicesResponse = Node.NO_CACHED_RESPONSE; // Set cache PMMR indicies parameters to no cache parameters this.cachePmmrIndiciesParameters = Node.NO_CACHE_PARAMETERS; // Set cached unspent outputs response to no cached response this.cachedUnspentOutputsResponse = Node.NO_CACHED_RESPONSE; // Set cache unspent outputs parameters to no cache parameters this.cacheUnspentOutputsParameters = Node.NO_CACHE_PARAMETERS; // Clear cached header responses this.cachedHeaderResponses = {}; // Clear cached kernel responses this.cachedKernelResponses = {}; } // Optimize cache optimizeCache() { // Check if current height is known if(this.currentHeight.getHeight() !== Node.UNKNOWN_HEIGHT) { // Get minimum used height threshold var minimumUsedHeightThreshold = this.currentHeight.getHeight().minus(Node.UNUSED_CACHED_RESPONSE_HEIGHT_VARIATION); // Check if minimum used height threshold is less than the first block height if(minimumUsedHeightThreshold.isLessThan(Consensus.FIRST_BLOCK_HEIGHT) === true) { // Set minimum used height threshold to the first block height minimumUsedHeightThreshold = new BigNumber(Consensus.FIRST_BLOCK_HEIGHT); } // Go through all cached header responses for(var height in this.cachedHeaderResponses) { if(this.cachedHeaderResponses.hasOwnProperty(height) === true) { // Check if header's height is less than the minimum used height threshold if(minimumUsedHeightThreshold.isGreaterThan(height) === true) { // Remove cached header response delete this.cachedHeaderResponses[height]; } } } // Go through all cached kernel responses for(var commit in this.cachedKernelResponses) { if(this.cachedKernelResponses.hasOwnProperty(commit) === true) { // Check if kernel doesn't exist or its height is less than the minimum used height threshold if(this.cachedKernelResponses[commit] === Node.NO_KERNEL_FOUND || minimumUsedHeightThreshold.isGreaterThan(this.cachedKernelResponses[commit]["height"]) === true) { // Remove cached kernel response delete this.cachedKernelResponses[commit]; } } } } } // Connection open event static get CONNECTION_OPEN_EVENT() { // Return connection open event return "NodeConnectionOpenEvent"; } // Connection warning event static get CONNECTION_WARNING_EVENT() { // Return connection warning event return "NodeConnectionWarningEvent"; } // Connection close event static get CONNECTION_CLOSE_EVENT() { // Return connection close event return "NodeConnectionCloseEvent"; } // Height change event static get HEIGHT_CHANGE_EVENT() { // Return height change event return "NodeHeightChangeEvent"; } // Settings change event static get SETTINGS_CHANGE_EVENT() { // Return settings change event return "NodeSettingsChangeEvent"; } // Unknown height static get UNKNOWN_HEIGHT() { // Return unknown height return null; } // No header found static get NO_HEADER_FOUND() { // Return no header found return null; } // No kernel found static get NO_KERNEL_FOUND() { // Return no kernel found return null; } // No connection close type static get NO_CONNECTION_CLOSE_TYPE() { // Return no connection close type return 0; } // Disconnected close type static get DISCONNECTED_CLOSE_TYPE() { // Return disconnected close type return Node.NO_CONNECTION_CLOSE_TYPE + 1; } // Incompatible warning type static get INCOMPATIBLE_WARNING_TYPE() { // Return incompatible warning type return 0; } // Invalid response warning type static get INVALID_RESPONSE_WARNING_TYPE() { // Return invalid response warning type return Node.INCOMPATIBLE_WARNING_TYPE + 1; } // Unauthorized warning type static get UNAUTHORIZED_WARNING_TYPE() { // Return unauthorized warning type return Node.INVALID_RESPONSE_WARNING_TYPE + 1; } // Minimum compatible node version static get MINIMUM_COMPATIBLE_NODE_VERSION() { // Check wallet type switch(Consensus.getWalletType()) { // MWC wallet case Consensus.MWC_WALLET_TYPE: // Return minimum compatible node version return "4.2.1"; // GRIN wallet case Consensus.GRIN_WALLET_TYPE: // Return minimum compatible node version return "5.0.1"; // EPIC wallet case Consensus.EPIC_WALLET_TYPE: // Return minimum compatible node version return "3.3.2"; } } // No value static get NO_VALUE() { // Return no value return null; } // No output found static get NO_OUTPUT_FOUND() { // Return no output found return null; } // Private // Reset reset() { // Check if update height timeout exists if(this.updateHeightTimeout !== Node.NO_TIMEOUT) { // Clear update height timeout clearTimeout(this.updateHeightTimeout); // Set update height timeout to no timeout this.updateHeightTimeout = Node.NO_TIMEOUT; } // Clear connected this.connected = false; // Set failed to connect this.failedToConnect = true; // Clear last height response timestamp this.lastHeightResponseTimestamp = Node.NO_HEIGHT_RESPONSE_TIMESTAMP; // Set current height's height to an unknown height this.currentHeight.setHeight(Node.UNKNOWN_HEIGHT); } // Connect connect(disconnectFirst = true) { // Check if disconnecting first if(disconnectFirst === true) { // Set empty address this.emptyAddress = this.getAddresses(Consensus.getNetworkType() === Consensus.MAINNET_NETWORK_TYPE)[0]["length"] === 0; // Check if address exists if(this.emptyAddress === false) { // Log message Log.logMessage(Language.getDefaultTranslation('Trying to connect to the node at %1$y.'), [ [ // Text this.getAddresses(Consensus.getNetworkType() === Consensus.MAINNET_NETWORK_TYPE)[0], // Is raw data true ] ]); } // Clear connected this.connected = false; // Clear failed to connect this.failedToConnect = false; // Clear last height response timestamp this.lastHeightResponseTimestamp = Node.NO_HEIGHT_RESPONSE_TIMESTAMP; // Clear version obtained this.versionObtained = false; } // Set self var self = this; // Get version this.getVersion().then(function(version) { // Check if disconnecting first if(disconnectFirst === true) { // Log message Log.logMessage(Language.getDefaultTranslation('Successfully connected to the node.')); // Log message Log.logMessage(Language.getDefaultTranslation('The node\'s version is %1$v.'), [ // Version version ]); } // Set version obtained self.versionObtained = true; // Check if version is compatible if(Node.isVersionGreaterThanOrEqual(version, Node.MINIMUM_COMPATIBLE_NODE_VERSION) === true) { // Get tip self.getTip(true).then(function(tip) { // Set current tip's height and hash self.currentHeight.setHeight(tip["height"]); self.currentHeight.setHash(tip["last_block_pushed"]); // Reset retry delay self.retryDelay = Node.INITIAL_RETRY_DELAY_MILLISECONDS; // Set connected self.connected = true; // Clear failed to connect self.failedToConnect = false; // Set last height response timestamp self.lastHeightResponseTimestamp = Common.getCurrentTimestamp(); // Trigger connection open event $(self).trigger(Node.CONNECTION_OPEN_EVENT); // Trigger height change event $(self).trigger(Node.HEIGHT_CHANGE_EVENT, [ // Height tip["height"], // Last block pushed tip["last_block_pushed"] ]); // Update height self.updateHeight(); // Catch errors }).catch(function(error) { }); } // Otherwise else { // Reset self.reset(); // Trigger connection warning event $(self).trigger(Node.CONNECTION_WARNING_EVENT, Node.INCOMPATIBLE_WARNING_TYPE); } // Catch errors }).catch(function(error) { }); } // Get response getResponse(method, responseType, parameters, value, validate, allowUnconnected = false, allowNotFoundError = false, allowMessageError = false) { // Set self var self = this; // Return promise return new Promise(function(resolve, reject) { // Check if not connected and unconnected isn't allowed if(self.connected === false && allowUnconnected === false) { // Reject connection error reject([ // Error type Node.CONNECTION_ERROR, // Response Node.NO_RESPONSE ]); } // Otherwise check if first address isn't a valid URL else if(Common.isValidUrl(self.getAddresses(Consensus.getNetworkType() === Consensus.MAINNET_NETWORK_TYPE)[0]) === false) { // Reset self.reset(); // Trigger connection close event $(self).trigger(Node.CONNECTION_CLOSE_EVENT, (self.versionObtained === false) ? Node.NO_CONNECTION_CLOSE_TYPE : Node.DISCONNECTED_CLOSE_TYPE); // Clear version obtained self.versionObtained = false; // Reject connection error reject([ // Error type Node.CONNECTION_ERROR, // Response Node.NO_RESPONSE ]); } // Otherwise else { // Get current ignore response index var index = self.ignoreResponseIndex++; // Check if current ignore resposne index is at the max safe integer if(index === Number.MAX_SAFE_INTEGER) { // Reset ignore response index self.ignoreResponseIndex = 0; } // Set ignore response var ignoreResponse = false; // Node settings change node index event $(self).one(Node.SETTINGS_CHANGE_EVENT + ".node" + index.toFixed(), function() { // Set ignore response ignoreResponse = true; }); // Node connection warning node index event $(self).one(Node.CONNECTION_WARNING_EVENT + ".node" + index.toFixed(), function() { // Set ignore response ignoreResponse = true; }); // Node connection close node index event $(self).one(Node.CONNECTION_CLOSE_EVENT + ".node" + index.toFixed(), function() { // Set ignore response ignoreResponse = true; }); // Get URL as the first address var url = self.getAddresses(Consensus.getNetworkType() === Consensus.MAINNET_NETWORK_TYPE)[0]; // Get proxy request var proxyRequest = Tor.isTorUrl(url) === true && Tor.isSupported() === false; // Return sending a JSON-RPC request return JsonRpc.sendRequest(self.getAddresses(Consensus.getNetworkType() === Consensus.MAINNET_NETWORK_TYPE).map(function(address) { // Return address with Tor proxy if required and the foreign API path return ((Tor.isTorUrl(address) === true && Tor.isSupported() === false) ? self.torProxy.getAddress() : "") + address + Node.FOREIGN_API_URL; }), method, parameters, (self.getSecret() === Node.NO_SECRET) ? {} : { // Authorization "Authorization": "Basic " + Base64.fromUint8Array((new TextEncoder()).encode(self.getUsername() + ":" + self.getSecret())) }, JsonRpc.DEFAULT_NUMBER_OF_ATTEMPTS, function() { // Return if ignoring response return ignoreResponse === true; }).then(function(response) { // Check if not ignoring response if(ignoreResponse === false) { // Turn off node settings change node index event $(self).off(Node.SETTINGS_CHANGE_EVENT + ".node" + index.toFixed()); // Turn off node connection warning node index event $(self).off(Node.CONNECTION_WARNING_EVENT + ".node" + index.toFixed()); // Turn off node connection close node index event $(self).off(Node.CONNECTION_CLOSE_EVENT + ".node" + index.toFixed()); } // Check if response isn't valid if(Object.isObject(response) === false || "Ok" in response === false) { // Set error to invalid response error var error = [ // Error type Node.INVALID_RESPONSE_ERROR, // Response response ]; // Check if not ignoring response if(ignoreResponse === false) { // Check if message error is allowed and error is a message error if(allowMessageError === true && Node.isMessageError(error) === true) { // Reject error reject(error); // Return return; } // Check if not found error isn't allowed or the error isn't a not found error if(allowNotFoundError === false || Node.isNotFoundError(error) === false) { // Reset self.reset(); // Trigger connection warning event $(self).trigger(Node.CONNECTION_WARNING_EVENT, Node.INVALID_RESPONSE_WARNING_TYPE); } } // Reject error reject(error); } // Otherwise else { // Set response to its value response = response["Ok"]; // Check if response doesn't match the expected response type if((responseType === Node.ARRAY_RESPONSE_TYPE && Array.isArray(response) === false) || (responseType === Node.OBJECT_RESPONSE_TYPE && Object.isObject(response) === false) || (responseType === Node.NO_RESPONSE_TYPE && response !== Node.API_NO_VALUE)) { // Check if not ignoring response if(ignoreResponse === false) { // Reset self.reset(); // Trigger connection warning event $(self).trigger(Node.CONNECTION_WARNING_EVENT, Node.INVALID_RESPONSE_WARNING_TYPE); } // Reject invalid response error reject([ // Error type Node.INVALID_RESPONSE_ERROR, // Response response ]); // Return return; } // Otherwise check if there is no value else if(value === Node.NO_VALUE) { // Check if response is no value if(response === Node.API_NO_VALUE) { // Set response value to no value var responseValue = Node.NO_VALUE; } // Otherwise else { // Check if not ignoring response if(ignoreResponse === false) { // Reset self.reset(); // Trigger connection warning event $(self).trigger(Node.CONNECTION_WARNING_EVENT, Node.INVALID_RESPONSE_WARNING_TYPE); } // Reject invalid response error reject([ // Error type Node.INVALID_RESPONSE_ERROR, // Response response ]); // Return return; } } // Otherwise check if there are multiple values else if(Array.isArray(value) === true) { // Check if response is an array if(Array.isArray(response) === true) { // Initialize response value var responseValue = []; // Go through all parts of the response for(var i = 0; i < response["length"]; ++i) { // Set first value var firstValue = true; // Go through all values for(var j = 0; j < value["length"]; ++j) { // Check if response part doesn't contain the value if(Object.isObject(response[i]) === false || value[j] in response[i] === false) { // Check if not ignoring response if(ignoreResponse === false) { // Reset self.reset(); // Trigger connection warning event $(self).trigger(Node.CONNECTION_WARNING_EVENT, Node.INVALID_RESPONSE_WARNING_TYPE); } // Reject invalid response error reject([ // Error type Node.INVALID_RESPONSE_ERROR, // Response response ]); // Return return; } // Check if value is invalid if(validate[j](response[i][value[j]], i) === false) { // Check if not ignoring response if(ignoreResponse === false) { // Reset self.reset(); // Trigger connection warning event $(self).trigger(Node.CONNECTION_WARNING_EVENT, Node.INVALID_RESPONSE_WARNING_TYPE); } // Reject invalid response error reject([ // Error type Node.INVALID_RESPONSE_ERROR, // Response response ]); // Return return; } // Check if first value if(firstValue === true) { // Clear first value firstValue = false; // Append to response value responseValue.push({}); } // Set value in response value responseValue[responseValue["length"] - 1][value[j]] = response[i][value[j]]; } } } // Otherwise check if response is an object else if(Object.isObject(response) === true) { // Initialize response value var responseValue = {}; // Go through all values for(var i = 0; i < value["length"]; ++i) { // Check if response doesn't contain the value if(value[i] in response === false) { // Check if not ignoring response if(ignoreResponse === false) { // Reset self.reset(); // Trigger connection warning event $(self).trigger(Node.CONNECTION_WARNING_EVENT, Node.INVALID_RESPONSE_WARNING_TYPE); } // Reject invalid response error reject([ // Error type Node.INVALID_RESPONSE_ERROR, // Response response ]); // Return return; } // Check if value is invalid if(validate[i](response[value[i]]) === false) { // Check if not ignoring response if(ignoreResponse === false) { // Reset self.reset(); // Trigger connection warning event $(self).trigger(Node.CONNECTION_WARNING_EVENT, Node.INVALID_RESPONSE_WARNING_TYPE); } // Reject invalid response error reject([ // Error type Node.INVALID_RESPONSE_ERROR, // Response response ]); // Return return; } // Set value in response value responseValue[value[i]] = response[value[i]]; } } // Otherwise else { // Check if not ignoring response if(ignoreResponse === false) { // Reset self.reset(); // Trigger connection warning event $(self).trigger(Node.CONNECTION_WARNING_EVENT, Node.INVALID_RESPONSE_WARNING_TYPE); } // Reject invalid response error reject([ // Error type Node.INVALID_RESPONSE_ERROR, // Response response ]); // Return return; } } // Otherwise else { // Check if response is an array if(Array.isArray(response) === true) { // Initialize response value var responseValue = []; // Go through all parts of the response for(var i = 0; i < response["length"]; ++i) { // Check if response part doesn't contain the value if(Object.isObject(response[i]) === false || value in response[i] === false) { // Check if not ignoring response if(ignoreResponse === false) { // Reset self.reset(); // Trigger connection warning event $(self).trigger(Node.CONNECTION_WARNING_EVENT, Node.INVALID_RESPONSE_WARNING_TYPE); } // Reject invalid response error reject([ // Error type Node.INVALID_RESPONSE_ERROR, // Response response ]); // Return return; } // Check if value is invalid if(validate(response[i][value], i) === false) { // Check if not ignoring response if(ignoreResponse === false) { // Reset self.reset(); // Trigger connection warning event $(self).trigger(Node.CONNECTION_WARNING_EVENT, Node.INVALID_RESPONSE_WARNING_TYPE); } // Reject invalid response error reject([ // Error type Node.INVALID_RESPONSE_ERROR, // Response response ]); // Return return; } // Set value in response value responseValue.push(response[i][value]); } } // Otherwise check if response is an object else if(Object.isObject(response) === true) { // Check if response doesn't contain the value if(value in response === false) { // Check if not ignoring response if(ignoreResponse === false) { // Reset self.reset(); // Trigger connection warning event $(self).trigger(Node.CONNECTION_WARNING_EVENT, Node.INVALID_RESPONSE_WARNING_TYPE); } // Reject invalid response error reject([ // Error type Node.INVALID_RESPONSE_ERROR, // Response response ]); // Return return; } // Check if value is invalid if(validate(response[value]) === false) { // Check if not ignoring response if(ignoreResponse === false) { // Reset self.reset(); // Trigger connection warning event $(self).trigger(Node.CONNECTION_WARNING_EVENT, Node.INVALID_RESPONSE_WARNING_TYPE); } // Reject invalid response error reject([ // Error type Node.INVALID_RESPONSE_ERROR, // Response response ]); // Return return; } // Set response value to value var responseValue = response[value]; } // Otherwise else { // Check if not ignoring response if(ignoreResponse === false) { // Reset self.reset(); // Trigger connection warning event $(self).trigger(Node.CONNECTION_WARNING_EVENT, Node.INVALID_RESPONSE_WARNING_TYPE); } // Reject invalid response error reject([ // Error type Node.INVALID_RESPONSE_ERROR, // Response response ]); // Return return; } } // Resolve response value resolve(responseValue); } // On error }).catch(function(responseStatusOrResponse) { // Check if ignoring response if(ignoreResponse === true) { // Reject connection error reject([ // Error type Node.CANCELED_RESPONSE_ERROR, // Response Node.NO_RESPONSE ]); // Return return; } // Otherwise else { // Turn off node settings change node index event $(self).off(Node.SETTINGS_CHANGE_EVENT + ".node" + index.toFixed()); // Turn off node connection warning node index event $(self).off(Node.CONNECTION_WARNING_EVENT + ".node" + index.toFixed()); // Turn off node connection close node index event $(self).off(Node.CONNECTION_CLOSE_EVENT + ".node" + index.toFixed()); } // Check if response is provided if(typeof responseStatusOrResponse !== "number") { // Set response status to HTTP ok status var responseStatus = Common.HTTP_OK_STATUS; } // Otherwise else { // Set response status to response status var responseStatus = responseStatusOrResponse; } // Check if response's status isn't no response or a timed out proxy response if(responseStatus !== Common.HTTP_NO_RESPONSE_STATUS || (proxyRequest === true && responseStatus !== Common.HTTP_BAD_GATEWAY_STATUS && responseStatus !== Common.HTTP_GATEWAY_TIMEOUT_STATUS && responseStatus !== Common.HTTP_NO_RESPONSE_STATUS)) { // Reset self.reset(); // Trigger connection warning event $(self).trigger(Node.CONNECTION_WARNING_EVENT, (self.usingCustomNode() === true && responseStatus === Common.HTTP_UNAUTHORIZED_STATUS) ? Node.UNAUTHORIZED_WARNING_TYPE : Node.INVALID_RESPONSE_WARNING_TYPE); // Reject bad response error reject([ // Error type Node.BAD_RESPONSE_ERROR, // Response responseStatus ]); } // Otherwise else { // Reset self.reset(); // Trigger connection close event $(self).trigger(Node.CONNECTION_CLOSE_EVENT, (self.versionObtained === false) ? Node.NO_CONNECTION_CLOSE_TYPE : Node.DISCONNECTED_CLOSE_TYPE); // Clear version obtained self.versionObtained = false; // Reject connection error reject([ // Error type Node.CONNECTION_ERROR, // Response Node.NO_RESPONSE ]); } }); } }); } // Get version getVersion() { // Set self var self = this; // Return promise return new Promise(function(resolve, reject) { // Return getting version from response return self.getResponse("get_version", Node.OBJECT_RESPONSE_TYPE, [], "node_version", function(nodeVersion) { // Check if node version isn't a string if(typeof nodeVersion !== "string") // Return false return false; // Return if node version is valid return Node.VERSION_PATTERN.test(nodeVersion) === true; }, true).then(function(value) { // Resolve value resolve(value); // Catch errors }).catch(function(error) { // Reject error reject(error); }); }); } // Get tip getTip(allowUnconnected = false) { // Set self var self = this; // Return promise return new Promise(function(resolve, reject) { // Return getting tip from response return self.getResponse("get_tip", Node.OBJECT_RESPONSE_TYPE, [], [ // Height "height", // Last block pushed "last_block_pushed" ], [ // Height validation function(height) { // Return if height is valid return (Common.isNumberString(height) === true || height instanceof BigNumber === true) && (new BigNumber(height)).isInteger() === true && (new BigNumber(height)).isGreaterThanOrEqualTo(Consensus.FIRST_BLOCK_HEIGHT) === true; }, // Last block pushed validation function(lastBlockPushed) { // Return if last block pushed is valid return Common.isHexString(lastBlockPushed) === true && Common.hexStringLength(lastBlockPushed) === RecentHeights.HEADER_HASH_LENGTH; } ], allowUnconnected).then(function(value) { // Check if current height is unknown if(self.currentHeight.getHeight() === Node.UNKNOWN_HEIGHT) { // Log message Log.logMessage(Language.getDefaultTranslation('The node\'s current height is %1$s.'), [ // Tip height (new BigNumber(value["height"])).toFixed() ]); } // Otherwise check if current height changed else if(self.currentHeight.getHeight().isEqualTo(value["height"]) === false) { // Log message Log.logMessage(Language.getDefaultTranslation('The node\'s new height is %1$s.'), [ // Tip height (new BigNumber(value["height"])).toFixed() ]); } // Resolve resolve({ // Height "height": new BigNumber(value["height"]), // Last block pushed "last_block_pushed": Common.fromHexString(value["last_block_pushed"]) }); // Catch errors }).catch(function(error) { // Reject error reject(error); }); }); } // Update height updateHeight(runImmediately = false) { // Check if update height timeout exists if(this.updateHeightTimeout !== Node.NO_TIMEOUT) { // Clear update height timeout clearTimeout(this.updateHeightTimeout); // Set update height timeout to no timeout this.updateHeightTimeout = Node.NO_TIMEOUT; } // Set self var self = this; // Check if run immediately if(runImmediately === true) { // Get tip this.getTip().then(function(tip) { // Set current tip's height and hash self.currentHeight.setHeight(tip["height"]); self.currentHeight.setHash(tip["last_block_pushed"]); // Set last height response timestamp self.lastHeightResponseTimestamp = Common.getCurrentTimestamp(); // Trigger height change event $(self).trigger(Node.HEIGHT_CHANGE_EVENT, [ // Height tip["height"], // Last block pushed tip["last_block_pushed"], // Ignore synced status true ]); // Update height self.updateHeight(); // Catch errors }).catch(function(error) { }); } // Otherwise else { // Set update height timeout this.updateHeightTimeout = setTimeout(function() { // Set update height timeout to no timeout self.updateHeightTimeout = Node.NO_TIMEOUT; // Get tip self.getTip().then(function(tip) { // Set current tip's height and hash self.currentHeight.setHeight(tip["height"]); self.currentHeight.setHash(tip["last_block_pushed"]); // Set last height response timestamp self.lastHeightResponseTimestamp = Common.getCurrentTimestamp(); // Trigger height change event $(self).trigger(Node.HEIGHT_CHANGE_EVENT, [ // Height tip["height"], // Last block pushed tip["last_block_pushed"] ]); // Update height self.updateHeight(); // Catch errors }).catch(function(error) { }); }, Node.UPDATE_HEIGHT_INTERVAL_SECONDS * Common.MILLISECONDS_IN_A_SECOND); } } // Get username getUsername(isMainnet) { // Check wallet type switch(Consensus.getWalletType()) { // MWC wallet case Consensus.MWC_WALLET_TYPE: // Check if mainnet if(isMainnet === true) // Return username return "mwcmain"; // Otherwise else // Return address return "mwcfloo"; // Break break; // GRIN wallet case Consensus.GRIN_WALLET_TYPE: // Return username return "grin"; // EPIC wallet case Consensus.EPIC_WALLET_TYPE: // Return username return "epic"; } } // Get secret getSecret() { // Check if not using a custom node if(this.usingCustomNode() === false) { // Check wallet type switch(Consensus.getWalletType()) { // MWC wallet case Consensus.MWC_WALLET_TYPE: // Return secret return "11ne3EAUtOXVKwhxm84U"; // GRIN or EPIC wallet case Consensus.GRIN_WALLET_TYPE: case Consensus.EPIC_WALLET_TYPE: // Return secret return Node.NO_SECRET; } } // Otherwise else { // Check if custom node's secret doesn't exist if(this.customNodeSecret["length"] === 0) // Return no secret return Node.NO_SECRET; // Otherwise else // Return custom node's secret return this.customNodeSecret; } } // Is version greater than or equal static isVersionGreaterThanOrEqual(firstVersion, secondVersion) { // Check if versions are equal if(firstVersion === secondVersion) // Return true return true; // Get first version components var firstVersionComponents = firstVersion.split(Node.VERSION_COMPONENT_PATTERN).map(function(versionComponent) { // Return version component as a number return (Common.isNumberString(versionComponent) === false || (new BigNumber(versionComponent)).isInteger() === false) ? Node.versionComponentTextToNumber(versionComponent).toFixed() : versionComponent; }); // Get second version components var secondVersionComponents = secondVersion.split(Node.VERSION_COMPONENT_PATTERN).map(function(versionComponent) { // Return version component as a number return (Common.isNumberString(versionComponent) === false || (new BigNumber(versionComponent)).isInteger() === false) ? Node.versionComponentTextToNumber(versionComponent).toFixed() : versionComponent; }); // Go through all version components for(var i = 0; i < Math.max(firstVersionComponents["length"], secondVersionComponents["length"]); ++i) { // Get version component numbers var firstVersionComponentNumber = new BigNumber((i < firstVersionComponents["length"]) ? firstVersionComponents[i] : Node.RELEASE_VERSION_COMPONENT_TEXT_VALUE.toFixed()); var secondVersionComponentNumber = new BigNumber((i < secondVersionComponents["length"]) ? secondVersionComponents[i] : Node.RELEASE_VERSION_COMPONENT_TEXT_VALUE.toFixed()); // Check if first version component number is greater than second version component number if(firstVersionComponentNumber.isGreaterThan(secondVersionComponentNumber) === true) { // Return true return true; } // Otherwise check if first version component number is less than second version component number else if(firstVersionComponentNumber.isLessThan(secondVersionComponentNumber) === true) { // Return false return false; } } // Return true return true; } // Version component text to number static versionComponentTextToNumber(versionComponentText) { // Make version component text lower case versionComponentText = versionComponentText.toLowerCase(); // Return version component text value or release version component text value if not found return (versionComponentText in Node.VERSION_COMPONENT_TEXT_VALUES === true) ? Node.VERSION_COMPONENT_TEXT_VALUES[versionComponentText] : Node.RELEASE_VERSION_COMPONENT_TEXT_VALUE; } // Is not found error static isNotFoundError(error) { // Return if error is that the value wasn't found return Array.isArray(error) === true && error["length"] > Node.ERROR_RESPONSE_INDEX && error[Node.ERROR_TYPE_INDEX] === Node.INVALID_RESPONSE_ERROR && Object.isObject(error[Node.ERROR_RESPONSE_INDEX]) === true && "Err" in error[Node.ERROR_RESPONSE_INDEX] === true && ((Object.isObject(error[Node.ERROR_RESPONSE_INDEX]["Err"]) === true && "NotFound" in error[Node.ERROR_RESPONSE_INDEX]["Err"] === true) || error[Node.ERROR_RESPONSE_INDEX]["Err"] === "NotFound"); } // Is message error static isMessageError(error) { // Return if error contains a message return Array.isArray(error) === true && error["length"] > Node.ERROR_RESPONSE_INDEX && error[Node.ERROR_TYPE_INDEX] === Node.INVALID_RESPONSE_ERROR && Object.isObject(error[Node.ERROR_RESPONSE_INDEX]) === true && "Err" in error[Node.ERROR_RESPONSE_INDEX] === true && Object.isObject(error[Node.ERROR_RESPONSE_INDEX]["Err"]) === true && "Internal" in error[Node.ERROR_RESPONSE_INDEX]["Err"] === true && typeof error[Node.ERROR_RESPONSE_INDEX]["Err"]["Internal"] === "string"; } // Version pattern static get VERSION_PATTERN() { // Return version pattern return /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[^\d].*)?$/u; } // Version component text values static get VERSION_COMPONENT_TEXT_VALUES() { // Return version component text values return { // Release "release": 0, // Beta "beta": -1, // Alpha "alpha": -2, // Dev "dev": -3, // Canary "canary": -4 }; } // Release version component text value static get RELEASE_VERSION_COMPONENT_TEXT_VALUE() { // Return release version component text value return Node.VERSION_COMPONENT_TEXT_VALUES["release"]; } // Version component pattern static get VERSION_COMPONENT_PATTERN() { // Return version component pattern return /[\.-]/ug; } // Connection error static get CONNECTION_ERROR() { // Return connection error return 0; } // Invalid response error static get INVALID_RESPONSE_ERROR() { // Return invalid response error return Node.CONNECTION_ERROR + 1; } // Bad response error static get BAD_RESPONSE_ERROR() { // Return bad response error return Node.INVALID_RESPONSE_ERROR + 1; } // Canceled response error static get CANCELED_RESPONSE_ERROR() { // Return canceled response error return Node.BAD_RESPONSE_ERROR + 1; } // Initial retry delay milliseconds static get INITIAL_RETRY_DELAY_MILLISECONDS() { // Return initial retry delay milliseconds return 1 * Common.MILLISECONDS_IN_A_SECOND; } // Maximum retry delay milliseconds static get MAXIMUM_RETRY_DELAY_MILLISECONDS() { // Return maximum retry delay milliseconds return Consensus.BLOCK_TIME_SECONDS * Common.MILLISECONDS_IN_A_SECOND; } // Retry delay scaling factor static get RETRY_DELAY_SCALING_FACTOR() { // Return retry delay scaling factor return 3; } // No timeout static get NO_TIMEOUT() { // Return no timeout return null; } // Settings use custom node name static get SETTINGS_USE_CUSTOM_NODE_NAME() { // Return settings use custom node name return "Use Custom Node"; } // Settings use custom node default value static get SETTINGS_USE_CUSTOM_NODE_DEFAULT_VALUE() { // Return settings use custom node default value return false; } // Settings custom node address name static get SETTINGS_CUSTOM_NODE_ADDRESS_NAME() { // Return settings custom node address name return "Custom Node Address"; } // Settings custom node address default value static get SETTINGS_CUSTOM_NODE_ADDRESS_DEFAULT_VALUE() { // Return settings custom node address default value return ""; } // Settings custom node secret name static get SETTINGS_CUSTOM_NODE_SECRET_NAME() { // Return settings custom node secret name return "Custom Node Secret"; } // Settings custom node secret default value static get SETTINGS_CUSTOM_NODE_SECRET_DEFAULT_VALUE() { // Return settings custom node secret default value return ""; } // No response static get NO_RESPONSE() { // Return no response return null; } // Error type index static get ERROR_TYPE_INDEX() { // Return error type index return 0; } // Error response index static get ERROR_RESPONSE_INDEX() { // Return error response index return Node.ERROR_TYPE_INDEX + 1; } // Foreign API version static get FOREIGN_API_VERSION() { // Return foreign API version return 2; } // Foreign API URL static get FOREIGN_API_URL() { // Return foreign API URL return "/v" + Node.FOREIGN_API_VERSION.toFixed() + "/foreign"; } // API no value static get API_NO_VALUE() { // Return API no value return null; } // No height response timestamp static get NO_HEIGHT_RESPONSE_TIMESTAMP() { // Return no height response timestamp return null; } // Maximum no height response duration seconds static get MAXIMUM_NO_HEIGHT_RESPONSE_DURATION_SECONDS() { // Return maximum no height response duration seconds return 3 * Node.UPDATE_HEIGHT_INTERVAL_SECONDS; } // No secret static get NO_SECRET() { // Return no secret return ""; } // Update height interval seconds static get UPDATE_HEIGHT_INTERVAL_SECONDS() { // Return update height interval seconds return Consensus.BLOCK_TIME_SECONDS; } // Array response type static get ARRAY_RESPONSE_TYPE() { // Return array response type return 0; } // Object response type static get OBJECT_RESPONSE_TYPE() { // Return object response type return Node.ARRAY_RESPONSE_TYPE + 1; } // NO response type static get NO_RESPONSE_TYPE() { // Return no response type return Node.OBJECT_RESPONSE_TYPE + 1; } // No cached response static get NO_CACHED_RESPONSE() { // Return no cached response return null; } // No cache parameters static get NO_CACHE_PARAMETERS() { // Return no cache parameters return null; } // No PMMR indices static get NO_PMMR_INDICES() { // Return no PMMR indices return null; } // Unused cached response height variation static get UNUSED_CACHED_RESPONSE_HEIGHT_VARIATION() { // Return unused cached response height variation return Consensus.BLOCK_HEIGHT_WEEK; } // First index static get FIRST_INDEX() { // Return first index return new BigNumber(1); } } // Main function // Set global object's node globalThis["Node"] = Node;