UNPKG

turtlecoin-wallet-backend

Version:

[![Build Status](https://travis-ci.org/turtlecoin/turtlecoin-wallet-backend-js.svg?branch=master)](https://travis-ci.org/turtlecoin/turtlecoin-wallet-backend-js)

480 lines (479 loc) 19.7 kB
"use strict"; // Copyright (c) 2018, Zpalmtree // // Please see the included LICENSE file for more information. var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); const _ = require("lodash"); const request = require("request-promise-native"); const events_1 = require("events"); const http = require("http"); const https = require("https"); const Assert_1 = require("./Assert"); const Types_1 = require("./Types"); const Config_1 = require("./Config"); const ValidateParameters_1 = require("./ValidateParameters"); const Logger_1 = require("./Logger"); const WalletError_1 = require("./WalletError"); class Daemon extends events_1.EventEmitter { /** * @param host The host to access the API on. Can be an IP, or a URL, for * example, 1.1.1.1, or blockapi.turtlepay.io * * @param port The port to access the API on. Normally 11898 for a TurtleCoin * daemon, 80 for a HTTP api, or 443 for a HTTPS api. * * @param isCacheApi You can optionally specify whether this API is a * blockchain cache API to save a couple of requests. * If you're not sure, do not specify this parameter - * we will work it out automatically. * * @param ssl You can optionally specify whether this API supports * ssl/tls/https to save a couple of requests. * If you're not sure, do not specify this parameter - * we will work it out automatically. */ constructor(host, port, isCacheApi, ssl) { super(); /** * Whether we should use https for our requests */ this.ssl = true; /** * Have we determined if we should be using ssl or not? */ this.sslDetermined = false; /** * Whether we're talking to a conventional daemon, or a blockchain cache API */ this.isCacheApi = false; /** * Have we determined if this is a cache API or not? */ this.isCacheApiDetermined = false; /** * The address node fees will go to */ this.feeAddress = ''; /** * The amount of the node fee in atomic units */ this.feeAmount = 0; /** * The amount of blocks the daemon we're connected to has */ this.localDaemonBlockCount = 0; /** * The amount of blocks the network has */ this.networkBlockCount = 0; /** * The amount of peers we have, incoming+outgoing */ this.peerCount = 0; /** * The hashrate of the last known local block */ this.lastKnownHashrate = 0; this.config = new Config_1.Config(); this.httpAgent = new http.Agent({ keepAlive: true, maxSockets: Infinity, keepAliveMsecs: 20000, }); this.httpsAgent = new https.Agent({ keepAlive: true, maxSockets: Infinity, keepAliveMsecs: 20000, }); /** * Last time the network height updated. If this goes over the configured * limit, we'll emit deadnode. */ this.lastUpdatedNetworkHeight = new Date(); /** * Last time the daemon height updated. If this goes over the configured * limit, we'll emit deadnode. */ this.lastUpdatedLocalHeight = new Date(); /** * Did our last contact with the daemon succeed. Set to true initially * so initial failure to connect will fire disconnect event. */ this.connected = true; this.setMaxListeners(0); Assert_1.assertString(host, 'host'); Assert_1.assertNumber(port, 'port'); Assert_1.assertBooleanOrUndefined(isCacheApi, 'isCacheApi'); Assert_1.assertBooleanOrUndefined(ssl, 'ssl'); this.host = host; this.port = port; /* Raw IP's very rarely support SSL. This fixes the warning from https://github.com/nodejs/node/pull/23329 */ if (/^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|$)){4}$/.test(this.host) && ssl === undefined) { ssl = false; } if (isCacheApi !== undefined) { this.isCacheApi = isCacheApi; this.isCacheApiDetermined = true; } if (ssl !== undefined) { this.ssl = ssl; this.sslDetermined = true; } } updateConfig(config) { this.config = Config_1.MergeConfig(config); } /** * Get the amount of blocks the network has */ getNetworkBlockCount() { return this.networkBlockCount; } /** * Get the amount of blocks the daemon we're connected to has */ getLocalDaemonBlockCount() { return this.localDaemonBlockCount; } /** * Initialize the daemon and the fee info */ init() { return __awaiter(this, void 0, void 0, function* () { /* Note - if one promise throws, the other will be cancelled */ yield Promise.all([this.updateDaemonInfo(), this.updateFeeInfo()]); if (this.networkBlockCount === 0) { this.emit('deadnode'); } }); } /** * Update the daemon info */ updateDaemonInfo() { return __awaiter(this, void 0, void 0, function* () { let info; const haveDeterminedSsl = this.sslDetermined; try { info = yield this.makeGetRequest('/info'); } catch (err) { Logger_1.logger.log('Failed to update daemon info: ' + err.toString(), Logger_1.LogLevel.INFO, [Logger_1.LogCategory.DAEMON]); const diff1 = (new Date().getTime() - this.lastUpdatedNetworkHeight.getTime()) / 1000; const diff2 = (new Date().getTime() - this.lastUpdatedLocalHeight.getTime()) / 1000; if (diff1 > this.config.maxLastUpdatedNetworkHeightInterval || diff2 > this.config.maxLastUpdatedLocalHeightInterval) { this.emit('deadnode'); } return; } /* Possibly determined daemon type was HTTPS, got a valid response, but not valid data. Manually set to http and try again. */ if (info.height === undefined && !haveDeterminedSsl) { this.sslDetermined = true; this.ssl = false; const diff1 = (new Date().getTime() - this.lastUpdatedNetworkHeight.getTime()) / 1000; const diff2 = (new Date().getTime() - this.lastUpdatedLocalHeight.getTime()) / 1000; if (diff1 > this.config.maxLastUpdatedNetworkHeightInterval || diff2 > this.config.maxLastUpdatedLocalHeightInterval) { this.emit('deadnode'); } return this.updateDaemonInfo(); } /* Are we talking to a cache API or not? */ if (!this.isCacheApiDetermined) { if (info.isCacheApi !== undefined && _.isBoolean(info.isCacheApi)) { this.isCacheApi = info.isCacheApi; this.isCacheApiDetermined = true; } else { this.isCacheApi = false; this.isCacheApiDetermined = true; } } /* Height returned is one more than the current height - but we don't want to overflow if the height returned is zero */ if (info.network_height !== 0) { info.network_height--; } if (this.localDaemonBlockCount !== info.height || this.networkBlockCount !== info.network_height) { this.emit('heightchange', info.height, info.network_height); this.lastUpdatedNetworkHeight = new Date(); this.lastUpdatedLocalHeight = new Date(); } else { const diff1 = (new Date().getTime() - this.lastUpdatedNetworkHeight.getTime()) / 1000; const diff2 = (new Date().getTime() - this.lastUpdatedLocalHeight.getTime()) / 1000; if (diff1 > this.config.maxLastUpdatedNetworkHeightInterval || diff2 > this.config.maxLastUpdatedLocalHeightInterval) { this.emit('deadnode'); } } this.localDaemonBlockCount = info.height; this.networkBlockCount = info.network_height; this.peerCount = info.incoming_connections_count + info.outgoing_connections_count; this.lastKnownHashrate = info.difficulty / this.config.blockTargetTime; }); } /** * Get the node fee and address */ nodeFee() { return [this.feeAddress, this.feeAmount]; } /** * @param blockHashCheckpoints Hashes of the last known blocks. Later * blocks (higher block height) should be * ordered at the front of the array. * * @param startHeight Height to start taking blocks from * @param startTimestamp Block timestamp to start taking blocks from * * Gets blocks from the daemon. Blocks are returned starting from the last * known block hash (if higher than the startHeight/startTimestamp) */ getWalletSyncData(blockHashCheckpoints, startHeight, startTimestamp, blockCount) { return __awaiter(this, void 0, void 0, function* () { let data; try { data = yield this.makePostRequest('/getwalletsyncdata', { blockCount, blockHashCheckpoints, skipCoinbaseTransactions: !this.config.scanCoinbaseTransactions, startHeight, startTimestamp, }); } catch (err) { Logger_1.logger.log('Failed to get wallet sync data: ' + err.toString(), Logger_1.LogLevel.INFO, [Logger_1.LogCategory.DAEMON]); return [[], false]; } /* The node is not dead if we're fetching blocks. */ if (data.items.length >= 0) { this.lastUpdatedNetworkHeight = new Date(); this.lastUpdatedLocalHeight = new Date(); } if (data.synced && data.topBlock && data.topBlock.height && data.topBlock.hash) { return [data.items.map(Types_1.Block.fromJSON), data.topBlock]; } return [data.items.map(Types_1.Block.fromJSON), true]; }); } /** * @returns Returns a mapping of transaction hashes to global indexes * * Get global indexes for the transactions in the range * [startHeight, endHeight] */ getGlobalIndexesForRange(startHeight, endHeight) { return __awaiter(this, void 0, void 0, function* () { if (this.isCacheApi) { throw new Error('This call is not supported on the cache api. The cache API ' + 'returns global indexes directly from /getWalletSyncData'); } try { const data = yield this.makePostRequest('/get_global_indexes_for_range', { startHeight, endHeight, }); const indexes = new Map(); for (const index of data.indexes) { indexes.set(index.key, index.value); } return indexes; } catch (err) { Logger_1.logger.log('Failed to get global indexes: ' + err.toString(), Logger_1.LogLevel.ERROR, Logger_1.LogCategory.DAEMON); return new Map(); } }); } getCancelledTransactions(transactionHashes) { return __awaiter(this, void 0, void 0, function* () { try { const data = yield this.makePostRequest('/get_transactions_status', { transactionHashes, }); return data.transactionsUnknown || []; } catch (err) { Logger_1.logger.log('Failed to get transactions status: ' + err.toString(), Logger_1.LogLevel.ERROR, Logger_1.LogCategory.DAEMON); return []; } }); } /** * Gets random outputs for the given amounts. requestedOuts per. Usually mixin+1. * * @returns Returns an array of amounts to global indexes and keys. There * should be requestedOuts indexes if the daemon fully fulfilled * our request. */ getRandomOutputsByAmount(amounts, requestedOuts) { return __awaiter(this, void 0, void 0, function* () { let data; try { if (this.isCacheApi) { data = yield this.makePostRequest('/randomOutputs', { amounts: amounts, mixin: requestedOuts, }); } else { const tmp = yield this.makePostRequest('/getrandom_outs', { amounts: amounts, outs_count: requestedOuts, }); data = tmp.outs || []; } } catch (err) { Logger_1.logger.log('Failed to get random outs: ' + err.toString(), Logger_1.LogLevel.ERROR, [Logger_1.LogCategory.TRANSACTIONS, Logger_1.LogCategory.DAEMON]); return []; } const outputs = []; for (const output of data) { const indexes = []; for (const outs of output.outs) { indexes.push([outs.global_amount_index, outs.out_key]); } /* Sort by output index to make it hard to determine real one */ outputs.push([output.amount, _.sortBy(indexes, ([index, key]) => index)]); } return outputs; }); } sendTransaction(rawTransaction) { return __awaiter(this, void 0, void 0, function* () { const result = yield this.makePostRequest('/sendrawtransaction', { tx_as_hex: rawTransaction, }); return result.status.toUpperCase() === 'OK'; }); } getConnectionInfo() { return { host: this.host, port: this.port, daemonType: this.isCacheApi ? Types_1.DaemonType.BlockchainCacheApi : Types_1.DaemonType.ConventionalDaemon, daemonTypeDetermined: this.isCacheApiDetermined, ssl: this.ssl, sslDetermined: this.sslDetermined, }; } /** * Update the fee address and amount */ updateFeeInfo() { return __awaiter(this, void 0, void 0, function* () { let feeInfo; try { feeInfo = yield this.makeGetRequest('/fee'); } catch (err) { Logger_1.logger.log('Failed to update fee info: ' + err.toString(), Logger_1.LogLevel.INFO, [Logger_1.LogCategory.DAEMON]); return; } if (feeInfo.address === '') { return; } const integratedAddressesAllowed = false; const err = ValidateParameters_1.validateAddresses(new Array(feeInfo.address), integratedAddressesAllowed, this.config).errorCode; if (err !== WalletError_1.WalletErrorCode.SUCCESS) { Logger_1.logger.log('Failed to validate address from daemon fee info: ' + err.toString(), Logger_1.LogLevel.WARNING, [Logger_1.LogCategory.DAEMON]); return; } if (feeInfo.amount > 0) { this.feeAddress = feeInfo.address; this.feeAmount = feeInfo.amount; } }); } makeGetRequest(endpoint) { return __awaiter(this, void 0, void 0, function* () { return this.makeRequest(endpoint, 'GET'); }); } makePostRequest(endpoint, body) { return __awaiter(this, void 0, void 0, function* () { return this.makeRequest(endpoint, 'POST', body); }); } /** * Makes a get request to the given endpoint */ makeRequest(endpoint, method, body) { return __awaiter(this, void 0, void 0, function* () { try { const protocol = this.sslDetermined ? (this.ssl ? 'https' : 'http') : 'https'; const data = yield request({ body: body, json: true, method: method, timeout: this.config.requestTimeout, /* Start by trying HTTPS if we haven't determined whether it's HTTPS or HTTP yet. */ url: `${protocol}://${this.host}:${this.port}${endpoint}`, agent: protocol === 'https' ? this.httpsAgent : this.httpAgent, }); /* Cool, https works. Store for later. */ if (!this.sslDetermined) { this.ssl = true; this.sslDetermined = true; } if (!this.connected) { this.emit('connect'); this.connected = true; } return data; } catch (err) { /* No point trying again with SSL - we already have decided what type it is. */ if (this.sslDetermined) { if (this.connected) { this.emit('disconnect', err); this.connected = false; } throw err; } try { const data = yield request({ body: body, json: true, method: method, timeout: this.config.requestTimeout, /* Lets try HTTP now. */ url: `http://${this.host}:${this.port}${endpoint}`, agent: this.httpAgent, }); this.ssl = false; this.sslDetermined = true; if (!this.connected) { this.emit('connect'); this.connected = true; } return data; } catch (err) { if (this.connected) { this.emit('disconnect', err); this.connected = false; } throw err; } } }); } } exports.Daemon = Daemon;