UNPKG

turtlecoin-wallet-backend

Version:

[![NPM](https://nodei.co/npm/turtlecoin-wallet-backend.png?compact=true)](https://npmjs.org/package/turtlecoin-wallet-backend)

1,126 lines 102 kB
"use strict"; // Copyright (c) 2018-2020, Zpalmtree // // Please see the included LICENSE file for more information. var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 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) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WalletBackend = void 0; /* eslint-disable max-len */ const events_1 = require("events"); const turtlecoin_utils_1 = require("turtlecoin-utils"); const fs = require("fs"); const _ = require("lodash"); const Metronome_1 = require("./Metronome"); const SubWallets_1 = require("./SubWallets"); const OpenWallet_1 = require("./OpenWallet"); const WalletEncryption_1 = require("./WalletEncryption"); const ValidateParameters_1 = require("./ValidateParameters"); const WalletSynchronizer_1 = require("./WalletSynchronizer"); const Config_1 = require("./Config"); const Logger_1 = require("./Logger"); const SynchronizationStatus_1 = require("./SynchronizationStatus"); const WalletError_1 = require("./WalletError"); const CnUtils_1 = require("./CnUtils"); const Transfer_1 = require("./Transfer"); const Constants_1 = require("./Constants"); const Utilities_1 = require("./Utilities"); const Assert_1 = require("./Assert"); /** * The WalletBackend provides an interface that allows you to synchronize * with a daemon, download blocks, process them, and pick out transactions that * belong to you. * It also allows you to inspect these transactions, view your balance, * send transactions, and more. * @noInheritDoc */ class WalletBackend extends events_1.EventEmitter { constructor(config, daemon, subWallets, walletSynchronizer) { super(); /** * Whether our wallet is synced. Used for selectively firing the sync/desync * event. */ this.synced = false; /** * Have we started the mainloop */ this.started = false; /** * Whether we should automatically keep the wallet optimized */ this.autoOptimize = true; /** * Should we perform auto optimization when next synced */ this.shouldPerformAutoOptimize = true; /** * Are we in the middle of an optimization? */ this.currentlyOptimizing = false; /** * Are we in the middle of a transaction? */ this.currentlyTransacting = false; /** * We only want to submit dead node once, then reset the flag when we * swap node or the node comes back online. */ this.haveEmittedDeadNode = false; /** * Previously prepared transactions for later sending. */ this.preparedTransactions = new Map(); this.config = config; this.daemon = daemon; this.subWallets = subWallets; this.walletSynchronizer = walletSynchronizer; this.setupEventHandlers(); this.setupMetronomes(); } /** * * This method opens a password protected wallet from a filepath. * The password protection follows the same format as wallet-api, * zedwallet-beta, and WalletBackend. It does NOT follow the same format * as turtle-service or zedwallet, and will be unable to open wallets * created with this program. * * Example: * ```javascript * const WB = require('turtlecoin-wallet-backend'); * * const daemon = new WB.Daemon('127.0.0.1', 11898); * * const [wallet, error] = await WB.WalletBackend.openWalletFromFile(daemon, 'mywallet.wallet', 'hunter2'); * * if (err) { * console.log('Failed to open wallet: ' + err.toString()); * } * ``` * @param daemon * @param filename The location of the wallet file on disk * @param password The password to use to decrypt the wallet. May be blank. * @param config */ static openWalletFromFile(daemon, filename, password, config) { return __awaiter(this, void 0, void 0, function* () { Logger_1.logger.log('Function openWalletFromFile called', Logger_1.LogLevel.DEBUG, Logger_1.LogCategory.GENERAL); Assert_1.assertString(filename, 'filename'); Assert_1.assertString(password, 'password'); const [walletJSON, error] = OpenWallet_1.openWallet(filename, password); if (error) { return [undefined, error]; } return WalletBackend.loadWalletFromJSON(daemon, walletJSON, config); }); } /** * * This method opens a password protected wallet from an encrypted string. * The password protection follows the same format as wallet-api, * zedwallet-beta, and WalletBackend. It does NOT follow the same format * as turtle-service or zedwallet, and will be unable to open wallets * created with this program. * * Example: * ```javascript * const WB = require('turtlecoin-wallet-backend'); * * const daemon = new WB.Daemon('127.0.0.1', 11898); * const data = 'ENCRYPTED_WALLET_STRING'; * * const [wallet, error] = await WB.WalletBackend.openWalletFromEncryptedString(daemon, data, 'hunter2'); * * if (err) { * console.log('Failed to open wallet: ' + err.toString()); * } * ``` * * @param daemon * @param data The encrypted string representing the wallet data * * @param password The password to use to decrypt the wallet. May be blank. * @param config */ static openWalletFromEncryptedString(daemon, data, password, config) { return __awaiter(this, void 0, void 0, function* () { Logger_1.logger.log('Function openWalletFromEncryptedString called', Logger_1.LogLevel.DEBUG, Logger_1.LogCategory.GENERAL); Assert_1.assertString(data, 'data'); Assert_1.assertString(password, 'password'); const [walletJSON, error] = WalletEncryption_1.WalletEncryption.decryptWalletFromString(data, password); if (error) { return [undefined, error]; } return WalletBackend.loadWalletFromJSON(daemon, walletJSON, config); }); } /** * Loads a wallet from a JSON encoded string. For the correct format for * the JSON to use, see https://github.com/turtlecoin/wallet-file-interaction * * You can obtain this JSON using [[toJSONString]]. * * Example: * ```javascript * const WB = require('turtlecoin-wallet-backend'); * * const daemon = new WB.Daemon('127.0.0.1', 11898); * * const [wallet, err] = await WB.WalletBackend.loadWalletFromJSON(daemon, json); * * if (err) { * console.log('Failed to load wallet: ' + err.toString()); * } * ``` * * @param daemon * * @param json Wallet info encoded as a JSON encoded string. Note * that this should be a *string*, NOT a JSON object. * This function will call `JSON.parse()`, so you should * not do that yourself. * @param config */ static loadWalletFromJSON(daemon, json, config) { return __awaiter(this, void 0, void 0, function* () { Logger_1.logger.log('Function loadWalletFromJSON called', Logger_1.LogLevel.DEBUG, Logger_1.LogCategory.GENERAL); const merged = Config_1.MergeConfig(config); Assert_1.assertString(json, 'json'); try { const wallet = JSON.parse(json, WalletBackend.reviver); if (yield wallet.isLedgerRequired()) { if (!merged.ledgerTransport) { return [undefined, new WalletError_1.WalletError(WalletError_1.WalletErrorCode.LEDGER_TRANSPORT_REQUIRED)]; } try { yield CnUtils_1.CryptoUtils(merged).init(); yield CnUtils_1.CryptoUtils(merged).fetchKeys(); const ledgerAddress = CnUtils_1.CryptoUtils(merged).address; if (!ledgerAddress) { return [undefined, new WalletError_1.WalletError(WalletError_1.WalletErrorCode.LEDGER_COULD_NOT_GET_KEYS)]; } if (wallet.getPrimaryAddress() !== (yield ledgerAddress.address())) { return [undefined, new WalletError_1.WalletError(WalletError_1.WalletErrorCode.LEDGER_WRONG_DEVICE_FOR_WALLET_FILE)]; } } catch (e) { return [undefined, new WalletError_1.WalletError(WalletError_1.WalletErrorCode.LEDGER_COULD_NOT_GET_KEYS)]; } } wallet.initAfterLoad(daemon, merged); return [wallet, undefined]; } catch (err) { return [undefined, new WalletError_1.WalletError(WalletError_1.WalletErrorCode.WALLET_FILE_CORRUPTED)]; } }); } /** * Imports a wallet from a 25 word mnemonic seed. * * Example: * ```javascript * const WB = require('turtlecoin-wallet-backend'); * * const daemon = new WB.Daemon('127.0.0.1', 11898); * * const seed = 'necklace went vials phone both haunted either eskimos ' + * 'dialect civilian western dabbing snout rustled balding ' + * 'puddle looking orbit rest agenda jukebox opened sarcasm ' + * 'solved eskimos'; * * const [wallet, err] = await WB.WalletBackend.importWalletFromSeed(daemon, 100000, seed); * * if (err) { * console.log('Failed to load wallet: ' + err.toString()); * } * ``` * * @param daemon * * @param scanHeight The height to begin scanning the blockchain from. * This can greatly increase sync speeds if given. * Defaults to zero if not given. * * @param mnemonicSeed The mnemonic seed to import. Should be a 25 word string. * @param config */ static importWalletFromSeed(daemon, scanHeight = 0, mnemonicSeed, config) { return __awaiter(this, void 0, void 0, function* () { Logger_1.logger.log('Function importWalletFromSeed called', Logger_1.LogLevel.DEBUG, Logger_1.LogCategory.GENERAL); Assert_1.assertNumber(scanHeight, 'scanHeight'); Assert_1.assertString(mnemonicSeed, 'mnemonicSeed'); const merged = Config_1.MergeConfig(config); let keys; try { keys = yield turtlecoin_utils_1.Address.fromMnemonic(mnemonicSeed, undefined, merged.addressPrefix); } catch (err) { return [undefined, new WalletError_1.WalletError(WalletError_1.WalletErrorCode.INVALID_MNEMONIC, err.toString())]; } if (scanHeight < 0) { return [undefined, new WalletError_1.WalletError(WalletError_1.WalletErrorCode.NEGATIVE_VALUE_GIVEN)]; } if (!Number.isInteger(scanHeight)) { return [undefined, new WalletError_1.WalletError(WalletError_1.WalletErrorCode.NON_INTEGER_GIVEN)]; } /* Can't sync from the current scan height, not newly created */ const newWallet = false; const wallet = yield WalletBackend.init(merged, daemon, yield keys.address(), scanHeight, newWallet, keys.view.privateKey, keys.spend.privateKey); return [wallet, undefined]; }); } /** * Imports a wallet from a pair of private keys. * * Example: * ```javascript * const WB = require('turtlecoin-wallet-backend'); * * const daemon = new WB.Daemon('127.0.0.1', 11898); * * const privateViewKey = 'ce4c27d5b135dc5310669b35e53efc9d50d92438f00c76442adf8c85f73f1a01'; * const privateSpendKey = 'f1b1e9a6f56241594ddabb243cdb39355a8b4a1a1c0343dde36f3b57835fe607'; * * const [wallet, err] = await WB.WalletBackend.importWalletFromSeed(daemon, 100000, privateViewKey, privateSpendKey); * * if (err) { * console.log('Failed to load wallet: ' + err.toString()); * } * ``` * * @param daemon * * @param scanHeight The height to begin scanning the blockchain from. * This can greatly increase sync speeds if given. * Defaults to zero. * * @param privateViewKey The private view key to import. Should be a 64 char hex string. * * @param privateSpendKey The private spend key to import. Should be a 64 char hex string. * @param config */ static importWalletFromKeys(daemon, scanHeight = 0, privateViewKey, privateSpendKey, config) { return __awaiter(this, void 0, void 0, function* () { Logger_1.logger.log('Function importWalletFromKeys called', Logger_1.LogLevel.DEBUG, Logger_1.LogCategory.GENERAL); Assert_1.assertNumber(scanHeight, 'scanHeight'); Assert_1.assertString(privateViewKey, 'privateViewKey'); Assert_1.assertString(privateSpendKey, 'privateSpendKey'); if (!Utilities_1.isHex64(privateViewKey) || !Utilities_1.isHex64(privateSpendKey)) { return [undefined, new WalletError_1.WalletError(WalletError_1.WalletErrorCode.INVALID_KEY_FORMAT)]; } const merged = Config_1.MergeConfig(config); let keys; try { keys = yield turtlecoin_utils_1.Address.fromKeys(privateSpendKey, privateViewKey, merged.addressPrefix); } catch (err) { return [undefined, new WalletError_1.WalletError(WalletError_1.WalletErrorCode.INVALID_KEY_FORMAT, err.toString())]; } if (scanHeight < 0) { return [undefined, new WalletError_1.WalletError(WalletError_1.WalletErrorCode.NEGATIVE_VALUE_GIVEN)]; } if (!Number.isInteger(scanHeight)) { return [undefined, new WalletError_1.WalletError(WalletError_1.WalletErrorCode.NON_INTEGER_GIVEN)]; } /* Can't sync from the current scan height, not newly created */ const newWallet = false; const wallet = yield WalletBackend.init(merged, daemon, yield keys.address(), scanHeight, newWallet, keys.view.privateKey, keys.spend.privateKey); return [wallet, undefined]; }); } /** * Imports a wallet from a Ledger hardware wallet * * Example: * ```javascript * const WB = require('turtlecoin-wallet-backend'); * const TransportNodeHID = require('@ledgerhq/hw-transport-node-hid').default * * const daemon = new WB.Daemon('127.0.0.1', 11898); * * const transport = await TransportNodeHID.create(); * * const [wallet, err] = await WB.WalletBackend.importWalletFromLedger(daemon, 100000, { * ledgerTransport: transport * }); * * if (err) { * console.log('Failed to load wallet: ' + err.toString()); * } * ``` * * @param daemon * * @param scanHeight The height to begin scanning the blockchain from. * This can greatly increase sync speeds if given. * Defaults to zero. * * @param config */ static importWalletFromLedger(daemon, scanHeight = 0, config) { return __awaiter(this, void 0, void 0, function* () { Logger_1.logger.log('Function importWalletFromLedger called', Logger_1.LogLevel.DEBUG, Logger_1.LogCategory.GENERAL); if (!config.ledgerTransport) { return [undefined, new WalletError_1.WalletError(WalletError_1.WalletErrorCode.LEDGER_TRANSPORT_REQUIRED)]; } Assert_1.assertNumber(scanHeight, 'scanHeight'); if (scanHeight < 0) { return [undefined, new WalletError_1.WalletError(WalletError_1.WalletErrorCode.NEGATIVE_VALUE_GIVEN)]; } if (!Number.isInteger(scanHeight)) { return [undefined, new WalletError_1.WalletError(WalletError_1.WalletErrorCode.NON_INTEGER_GIVEN)]; } const merged = Config_1.MergeConfig(config); let address; try { yield CnUtils_1.CryptoUtils(merged).init(); yield CnUtils_1.CryptoUtils(merged).fetchKeys(); const tmpAddress = CnUtils_1.CryptoUtils(merged).address; if (tmpAddress) { address = tmpAddress; } else { return [undefined, new WalletError_1.WalletError(WalletError_1.WalletErrorCode.LEDGER_COULD_NOT_GET_KEYS)]; } } catch (e) { return [undefined, new WalletError_1.WalletError(WalletError_1.WalletErrorCode.LEDGER_COULD_NOT_GET_KEYS)]; } /* Can't sync from the current scan height, not newly created */ const newWallet = false; const wallet = yield WalletBackend.init(merged, daemon, yield address.address(), scanHeight, newWallet, address.view.privateKey, '0'.repeat(64)); return [wallet, undefined]; }); } /** * This method imports a wallet you have previously created, in a 'watch only' * state. This wallet can view incoming transactions, but cannot send * transactions. It also cannot view outgoing transactions, so balances * may appear incorrect. * This is useful for viewing your balance whilst not risking your funds * or private keys being stolen. * * Example: * ```javascript * const WB = require('turtlecoin-wallet-backend'); * * const daemon = new WB.Daemon('127.0.0.1', 11898); * * const privateViewKey = 'ce4c27d5b135dc5310669b35e53efc9d50d92438f00c76442adf8c85f73f1a01'; * * const address = 'TRTLv2Fyavy8CXG8BPEbNeCHFZ1fuDCYCZ3vW5H5LXN4K2M2MHUpTENip9bbavpHvvPwb4NDkBWrNgURAd5DB38FHXWZyoBh4wW'; * * const [wallet, err] = await WB.WalletBackend.importViewWallet(daemon, 100000, privateViewKey, address); * * if (err) { * console.log('Failed to load wallet: ' + err.toString()); * } * ``` * * @param daemon * * @param scanHeight The height to begin scanning the blockchain from. * This can greatly increase sync speeds if given. * Defaults to zero. * @param privateViewKey The private view key of this view wallet. Should be a 64 char hex string. * * @param address The public address of this view wallet. * @param config */ static importViewWallet(daemon, scanHeight = 0, privateViewKey, address, config) { return __awaiter(this, void 0, void 0, function* () { Logger_1.logger.log('Function importViewWallet called', Logger_1.LogLevel.DEBUG, Logger_1.LogCategory.GENERAL); Assert_1.assertNumber(scanHeight, 'scanHeight'); Assert_1.assertString(privateViewKey, 'privateViewKey'); Assert_1.assertString(address, 'address'); if (!Utilities_1.isHex64(privateViewKey)) { return [undefined, new WalletError_1.WalletError(WalletError_1.WalletErrorCode.INVALID_KEY_FORMAT)]; } const integratedAddressesAllowed = false; const err = yield ValidateParameters_1.validateAddresses(new Array(address), integratedAddressesAllowed, Config_1.MergeConfig(config)); if (!_.isEqual(err, WalletError_1.SUCCESS)) { return [undefined, err]; } if (scanHeight < 0) { return [undefined, new WalletError_1.WalletError(WalletError_1.WalletErrorCode.NEGATIVE_VALUE_GIVEN)]; } if (!Number.isInteger(scanHeight)) { return [undefined, new WalletError_1.WalletError(WalletError_1.WalletErrorCode.NON_INTEGER_GIVEN)]; } /* Can't sync from the current scan height, not newly created */ const newWallet = false; const wallet = yield WalletBackend.init(Config_1.MergeConfig(config), daemon, address, scanHeight, newWallet, privateViewKey); return [wallet, undefined]; }); } /** * This method creates a new wallet instance with a random key pair. * * Example: * ```javascript * const WB = require('turtlecoin-wallet-backend'); * * const daemon = new WB.Daemon('127.0.0.1', 11898); * * const wallet = await WB.WalletBackend.createWallet(daemon); * ``` * * @param daemon * @param config */ static createWallet(daemon, config) { return __awaiter(this, void 0, void 0, function* () { Logger_1.logger.log('Function createWallet called', Logger_1.LogLevel.DEBUG, Logger_1.LogCategory.GENERAL); const newWallet = true; const scanHeight = 0; const merged = Config_1.MergeConfig(config); let address = yield turtlecoin_utils_1.Address.fromEntropy(undefined, undefined, merged.addressPrefix); if (merged.ledgerTransport) { yield CnUtils_1.CryptoUtils(merged).init(); yield CnUtils_1.CryptoUtils(merged).fetchKeys(); const ledgerAddress = CnUtils_1.CryptoUtils(merged).address; if (ledgerAddress) { address = ledgerAddress; } else { throw new Error('Could not create wallet from Ledger transport'); } } return WalletBackend.init(merged, daemon, yield address.address(), scanHeight, newWallet, address.view.privateKey, address.spend.privateKey); }); } /* Utility function for nicer JSON parsing function */ static reviver(key, value) { return key === '' ? WalletBackend.fromJSON(value) : value; } /* Loads a wallet from a WalletBackendJSON */ static fromJSON(json) { const wallet = Object.create(WalletBackend.prototype); const version = json.walletFileFormatVersion; if (version !== Constants_1.WALLET_FILE_FORMAT_VERSION) { throw new Error('Unsupported wallet file format version!'); } return Object.assign(wallet, { subWallets: SubWallets_1.SubWallets.fromJSON(json.subWallets), walletSynchronizer: WalletSynchronizer_1.WalletSynchronizer.fromJSON(json.walletSynchronizer), }); } /** * @param config * @param daemon * @param address * @param newWallet Are we creating a new wallet? If so, it will start * syncing from the current time. * * @param scanHeight The height to begin scanning the blockchain from. * This can greatly increase sync speeds if given. * Set to zero if `newWallet` is `true`. * @param privateViewKey * @param privateSpendKey Omit this parameter to create a view wallet. * */ static init(config, daemon, address, scanHeight, newWallet, privateViewKey, privateSpendKey) { return __awaiter(this, void 0, void 0, function* () { daemon.updateConfig(config); const subWallets = yield SubWallets_1.SubWallets.init(config, address, scanHeight, newWallet, privateViewKey, privateSpendKey); let timestamp = 0; if (newWallet) { timestamp = Utilities_1.getCurrentTimestampAdjusted(); } const walletSynchronizer = new WalletSynchronizer_1.WalletSynchronizer(daemon, subWallets, timestamp, scanHeight, privateViewKey, config); const result = new WalletBackend(config, daemon, subWallets, walletSynchronizer); if (!result.usingNativeCrypto()) { Logger_1.logger.log('Wallet is not using native crypto. Syncing could be much slower than normal.', Logger_1.LogLevel.WARNING, Logger_1.LogCategory.GENERAL); } return result; }); } /** * Swaps the currently connected daemon with a different one. If the wallet * is currently started, it will remain started after the node is swapped, * if it is currently stopped, it will remain stopped. * * Example: * ```javascript * const daemon = new WB.Daemon('blockapi.turtlepay.io', 443); * await wallet.swapNode(daemon); * const daemonInfo = wallet.getDaemonConnectionInfo(); * console.log(`Connected to ${daemonInfo.ssl ? 'https://' : 'http://'}${daemonInfo.host}:${daemonInfo.port}`); * ``` */ swapNode(newDaemon) { return __awaiter(this, void 0, void 0, function* () { Logger_1.logger.log('Function swapNode called', Logger_1.LogLevel.DEBUG, Logger_1.LogCategory.GENERAL); Logger_1.logger.log(`Swapping node from ${this.daemon.getConnectionString()} to ${newDaemon.getConnectionString()}`, Logger_1.LogLevel.DEBUG, Logger_1.LogCategory.DAEMON); const shouldRestart = this.started; yield this.stop(); /* Ensuring we don't double emit if same daemon instance is given */ if (this.daemon !== newDaemon) { /* Passing through events from daemon to users */ newDaemon.on('disconnect', () => { this.emit('disconnect'); }); newDaemon.on('connect', () => { this.emit('connect'); }); } this.daemon = newDaemon; this.daemon.updateConfig(this.config); /* Discard blocks which are stored which may cause issues, for example, * if we swap from a cache node to a non cache node, * /getGlobalIndexesForRange will fail. */ this.discardStoredBlocks(); this.haveEmittedDeadNode = false; if (shouldRestart) { yield this.start(); } }); } /** * Gets information on the currently connected daemon - It's host, port, * daemon type, and ssl presence. * This can be helpful if you are taking arbitary host/port from a user, * and wish to display the daemon type they are connecting to once we * have figured it out. * Note that the `ssl` and `daemonType` variables may have not been * determined yet - If you have not awaited [[start]] yet, or if the daemon * is having connection issues. * * For this reason, there are two additional properties - `sslDetermined`, * and `daemonTypeDetermined` which let you verify that we have managed * to contact the daemon and detect its specifics. * * Example: * ```javascript * const daemonInfo = wallet.getDaemonConnectionInfo(); * console.log(`Connected to ${daemonInfo.ssl ? 'https://' : 'http://'}${daemonInfo.host}:${daemonInfo.port}`); * ``` */ getDaemonConnectionInfo() { Logger_1.logger.log('Function getDaemonConnectionInfo called', Logger_1.LogLevel.DEBUG, Logger_1.LogCategory.GENERAL); return this.daemon.getConnectionInfo(); } /** * Performs the same operation as reset(), but uses the initial scan height * or timestamp. For example, if you created your wallet at block 800,000, * this method would start rescanning from then. * * This function will return once the wallet has been successfully reset, * and syncing has began again. * * Example: * ```javascript * await wallet.rescan(); * ``` */ rescan() { Logger_1.logger.log('Function rescan called', Logger_1.LogLevel.DEBUG, Logger_1.LogCategory.GENERAL); const [scanHeight, scanTimestamp] = this.walletSynchronizer.getScanHeights(); return this.reset(scanHeight, scanTimestamp); } /** * * Discard all transaction data, and begin scanning the wallet again * from the scanHeight or timestamp given. Defaults to a height of zero, * if not given. * * This function will return once the wallet has been successfully reset, * and syncing has began again. * * Example: * ```javascript * await wallet.reset(123456); * ``` * * @param scanHeight The scan height to begin scanning transactions from * @param scanTimestamp The timestamp to being scanning transactions from */ reset(scanHeight = 0, scanTimestamp = 0) { return __awaiter(this, void 0, void 0, function* () { Logger_1.logger.log('Function reset called', Logger_1.LogLevel.DEBUG, Logger_1.LogCategory.GENERAL); Assert_1.assertNumber(scanHeight, 'scanHeight'); Assert_1.assertNumber(scanTimestamp, 'scanTimestamp'); const shouldRestart = this.started; yield this.stop(); yield this.walletSynchronizer.reset(scanHeight, scanTimestamp); yield this.subWallets.reset(scanHeight, scanTimestamp); if (shouldRestart) { yield this.start(); } this.emit('heightchange', this.walletSynchronizer.getHeight(), this.daemon.getLocalDaemonBlockCount(), this.daemon.getNetworkBlockCount()); }); } /** * This function works similarly to both [[reset]] and [[rescan]]. * * The difference is that while reset and rescan discard all progress before * the specified height, and then continues syncing from there, rewind * instead retains the information previous, and only removes information * after the rewind height. * * This can be helpful if you suspect a transaction has been missed by * the sync process, and want to only rescan a small section of blocks. * * Example: * ```javascript * await wallet.rewind(123456); * ``` * * @param scanHeight The scan height to rewind to */ rewind(scanHeight = 0) { return __awaiter(this, void 0, void 0, function* () { Logger_1.logger.log('Function rewind called', Logger_1.LogLevel.DEBUG, Logger_1.LogCategory.GENERAL); Assert_1.assertNumber(scanHeight, 'scanHeight'); const shouldRestart = this.started; yield this.stop(); yield this.walletSynchronizer.rewind(scanHeight); yield this.subWallets.rewind(scanHeight); if (shouldRestart) { yield this.start(); } this.emit('heightchange', this.walletSynchronizer.getHeight(), this.daemon.getLocalDaemonBlockCount(), this.daemon.getNetworkBlockCount()); }); } /** * Adds a subwallet to the wallet container. Must not be used on a view * only wallet. For more information on subwallets, see https://docs.turtlecoin.lol/developer/subwallets * * Example: * ```javascript * const [address, error] = await wallet.addSubWallet(); * * if (!error) { * console.log(`Created subwallet with address of ${address}`); * } * ``` * * @returns Returns the newly created address or an error. */ addSubWallet() { return __awaiter(this, void 0, void 0, function* () { Logger_1.logger.log('Function addSubWallet called', Logger_1.LogLevel.DEBUG, Logger_1.LogCategory.GENERAL); if (!(yield this.subwalletsSupported())) { return [undefined, new WalletError_1.WalletError(WalletError_1.WalletErrorCode.LEDGER_SUBWALLETS_NOT_SUPPORTED)]; } const currentHeight = this.walletSynchronizer.getHeight(); return this.subWallets.addSubWallet(currentHeight); }); } /** * Imports a subwallet to the wallet container. Must not be used on a view * only wallet. For more information on subwallets, see https://docs.turtlecoin.lol/developer/subwallets * * Example: * ```javascript * const [address, error] = await wallet.importSubWallet('c984628484a1a5eaab4cfb63831b2f8ac8c3a56af2102472ab35044b46742501'); * * if (!error) { * console.log(`Imported subwallet with address of ${address}`); * } else { * console.log(`Failed to import subwallet: ${error.toString()}`); * } * ``` * * @param privateSpendKey The private spend key of the subwallet to import * @param scanHeight The scan height to start scanning this subwallet from. * If the scan height is less than the wallets current * height, the entire wallet will be rewound to that height, * and will restart syncing. If not specified, this defaults * to the current height. * @returns Returns the newly created address or an error. */ importSubWallet(privateSpendKey, scanHeight) { return __awaiter(this, void 0, void 0, function* () { Logger_1.logger.log('Function importSubWallet called', Logger_1.LogLevel.DEBUG, Logger_1.LogCategory.GENERAL); if (!(yield this.subwalletsSupported())) { return [undefined, new WalletError_1.WalletError(WalletError_1.WalletErrorCode.LEDGER_SUBWALLETS_NOT_SUPPORTED)]; } const currentHeight = this.walletSynchronizer.getHeight(); if (scanHeight === undefined) { scanHeight = currentHeight; } Assert_1.assertString(privateSpendKey, 'privateSpendKey'); Assert_1.assertNumber(scanHeight, 'scanHeight'); if (!Utilities_1.isHex64(privateSpendKey)) { return [undefined, new WalletError_1.WalletError(WalletError_1.WalletErrorCode.INVALID_KEY_FORMAT)]; } const [error, address] = yield this.subWallets.importSubWallet(privateSpendKey, scanHeight); /* If the import height is lower than the current height then we need * to go back and rescan those blocks with the new subwallet. */ if (!error) { if (currentHeight > scanHeight) { yield this.rewind(scanHeight); } } /* Since we destructured the components, compiler can no longer figure * out it's either [string, undefined], or [undefined, WalletError] - * it could possibly be [string, WalletError] */ return [error, address]; }); } /** * Imports a view only subwallet to the wallet container. Must not be used * on a non view wallet. For more information on subwallets, see https://docs.turtlecoin.lol/developer/subwallets * * Example: * ```javascript * const [address, error] = await wallet.importViewSubWallet('c984628484a1a5eaab4cfb63831b2f8ac8c3a56af2102472ab35044b46742501'); * * if (!error) { * console.log(`Imported view subwallet with address of ${address}`); * } else { * console.log(`Failed to import view subwallet: ${error.toString()}`); * } * ``` * * @param publicSpendKey The public spend key of the subwallet to import * @param scanHeight The scan height to start scanning this subwallet from. * If the scan height is less than the wallets current * height, the entire wallet will be rewound to that height, * and will restart syncing. If not specified, this defaults * to the current height. * @returns Returns the newly created address or an error. */ importViewSubWallet(publicSpendKey, scanHeight) { return __awaiter(this, void 0, void 0, function* () { Logger_1.logger.log('Function importViewSubWallet called', Logger_1.LogLevel.DEBUG, Logger_1.LogCategory.GENERAL); if (!(yield this.subwalletsSupported())) { return [undefined, new WalletError_1.WalletError(WalletError_1.WalletErrorCode.LEDGER_SUBWALLETS_NOT_SUPPORTED)]; } const currentHeight = this.walletSynchronizer.getHeight(); if (scanHeight === undefined) { scanHeight = currentHeight; } Assert_1.assertString(publicSpendKey, 'publicSpendKey'); Assert_1.assertNumber(scanHeight, 'scanHeight'); if (!Utilities_1.isHex64(publicSpendKey)) { return [undefined, new WalletError_1.WalletError(WalletError_1.WalletErrorCode.INVALID_KEY_FORMAT)]; } const [error, address] = yield this.subWallets.importViewSubWallet(publicSpendKey, scanHeight); /* If the import height is lower than the current height then we need * to go back and rescan those blocks with the new subwallet. */ if (!error) { if (currentHeight > scanHeight) { yield this.rewind(scanHeight); } } /* Since we destructured the components, compiler can no longer figure * out it's either [string, undefined], or [undefined, WalletError] - * it could possibly be [string, WalletError] */ return [error, address]; }); } /** * Removes the subwallet specified from the wallet container. If you have * not backed up the private keys for this subwallet, all funds in it * will be lost. * * Example: * ```javascript * const error = await wallet.deleteSubWallet('TRTLv2txGW8daTunmAVV6dauJgEv1LezM2Hse7EUD5c11yKHsNDrzQ5UWNRmu2ToQVhDcr82ZPVXy4mU5D7w9RmfR747KeXD3UF'); * * if (error) { * console.log(`Failed to delete subwallet: ${error.toString()}`); * } * ``` * * @param address The subwallet address to remove */ deleteSubWallet(address) { return __awaiter(this, void 0, void 0, function* () { Logger_1.logger.log('Function deleteSubWallet called', Logger_1.LogLevel.DEBUG, Logger_1.LogCategory.GENERAL); if (!(yield this.subwalletsSupported())) { return new WalletError_1.WalletError(WalletError_1.WalletErrorCode.LEDGER_SUBWALLETS_NOT_SUPPORTED); } Assert_1.assertString(address, 'address'); const err = yield ValidateParameters_1.validateAddresses(new Array(address), false, this.config); if (!_.isEqual(err, WalletError_1.SUCCESS)) { return err; } return this.subWallets.deleteSubWallet(address); }); } /** * Returns the number of subwallets in this wallet. * * Example: * ```javascript * const count = wallet.getWalletCount(); * * console.log(`Wallet has ${count} subwallets`); * ``` */ getWalletCount() { Logger_1.logger.log('Function getWalletCount called', Logger_1.LogLevel.DEBUG, Logger_1.LogCategory.GENERAL); return this.subWallets.getWalletCount(); } /** * Gets the wallet, local daemon, and network block count * * Example: * ```javascript * const [walletBlockCount, localDaemonBlockCount, networkBlockCount] = * wallet.getSyncStatus(); * ``` */ getSyncStatus() { Logger_1.logger.log('Function getSyncStatus called', Logger_1.LogLevel.DEBUG, Logger_1.LogCategory.GENERAL); return [ this.walletSynchronizer.getHeight(), this.daemon.getLocalDaemonBlockCount(), this.daemon.getNetworkBlockCount(), ]; } /** * Converts the wallet into a JSON string. This can be used to later restore * the wallet with [[loadWalletFromJSON]]. * * Example: * ```javascript * const walletData = wallet.toJSONString(); * ``` */ toJSONString() { Logger_1.logger.log('Function toJSONString called', Logger_1.LogLevel.DEBUG, Logger_1.LogCategory.GENERAL); return JSON.stringify(this, null, 4); } /** * * Most people don't mine blocks, so by default we don't scan them. If * you want to scan them, flip it on/off here. * * Example: * ```javascript * wallet.scanCoinbaseTransactions(true); * ``` * * @param shouldScan Should we scan coinbase transactions? */ scanCoinbaseTransactions(shouldScan) { Logger_1.logger.log('Function scanCoinbaseTransactions called', Logger_1.LogLevel.DEBUG, Logger_1.LogCategory.GENERAL); Assert_1.assertBoolean(shouldScan, 'shouldScan'); /* We are not currently scanning coinbase transactions, and the caller * just turned it on. So, we need to discard stored blocks that don't * have the coinbase transaction property. */ if (!this.config.scanCoinbaseTransactions && shouldScan) { this.discardStoredBlocks(); } this.config.scanCoinbaseTransactions = shouldScan; this.daemon.updateConfig(this.config); } /** * Sets the log level. Log messages below this level are not shown. * * Logging by default occurs to stdout. See [[setLoggerCallback]] to modify this, * or gain more control over what is logged. * * Example: * ```javascript * wallet.setLogLevel(WB.LogLevel.DEBUG); * ``` * * @param logLevel The level to log messages at. */ setLogLevel(logLevel) { Logger_1.logger.log('Function setLogLevel called', Logger_1.LogLevel.DEBUG, Logger_1.LogCategory.GENERAL); Logger_1.logger.setLogLevel(logLevel); } /** * This flag will automatically send fusion transactions when needed * to keep your wallet permanently optimized. * * The downsides are that sometimes your wallet will 'unexpectedly' have * locked funds. * * The upside is that when you come to sending a large transaction, it * should nearly always succeed. * * This flag is ENABLED by default. * * Example: * ```javascript * wallet.enableAutoOptimization(false); * ``` * * @param shouldAutoOptimize Should we automatically keep the wallet optimized? */ enableAutoOptimization(shouldAutoOptimize) { Logger_1.logger.log('Function enableAutoOptimization called', Logger_1.LogLevel.DEBUG, Logger_1.LogCategory.GENERAL); Assert_1.assertBoolean(shouldAutoOptimize, 'shouldAutoOptimize'); this.autoOptimize = shouldAutoOptimize; } /** * Returns a string indicating the type of cryptographic functions being used. * * Example: * ```javascript * const cryptoType = wallet.getCryptoType(); * * console.log(`Wallet is using the ${cryptoType} cryptographic library.`); * ``` */ getCryptoType() { Logger_1.logger.log('Function getCryptoType called', Logger_1.LogLevel.DEBUG, Logger_1.LogCategory.GENERAL); const type = new turtlecoin_utils_1.Crypto().type; switch (type) { case turtlecoin_utils_1.CryptoType.NODEADDON: return 'C++'; case turtlecoin_utils_1.CryptoType.JS: return 'js'; case turtlecoin_utils_1.CryptoType.WASM: return 'wasm'; case turtlecoin_utils_1.CryptoType.WASMJS: return 'wasmjs'; case turtlecoin_utils_1.CryptoType.EXTERNAL: return 'user-defined'; case turtlecoin_utils_1.CryptoType.MIXED: return 'mixed'; case turtlecoin_utils_1.CryptoType.UNKNOWN: default: return 'unknown'; } } /** * Returns a boolean indicating whether or not the wallet is using native crypto * * Example: * ```javascript * const native = wallet.usingNativeCrypto(); * * if (native) { * console.log('Wallet is using native cryptographic code.'); * } * ``` */ usingNativeCrypto() { Logger_1.logger.log('Function usingNativeCrypto called', Logger_1.LogLevel.DEBUG, Logger_1.LogCategory.GENERAL); return turtlecoin_utils_1.Crypto.isNative; } /** * Sets a callback to be used instead of console.log for more fined control * of the logging output. * * Ensure that you have enabled logging for this function to take effect. * See [[setLogLevel]] for more details. * * Example: * ```javascript * wallet.setLoggerCallback((prettyMessage, message, level, categories) => { * if (categories.includes(WB.LogCategory.SYNC)) { * console.log(prettyMessage); * } * }); * ``` * * @param callback The callback to use for log messages * @param callback.prettyMessage A nicely formatted log message, with timestamp, levels, and categories * @param callback.message The raw log message * @param callback.level The level at which the message was logged at * @param callback.categories The categories this log message falls into */ setLoggerCallback(callback) { Logger_1.logger.log('Function setLoggerCallback called', Logger_1.LogLevel.DEBUG, Logger_1.LogCategory.GENERAL); Logger_1.logger.setLoggerCallback(callback); } /** * Provide a function to process blocks instead of the inbuilt one. The * only use for this is to leverage native code to provide quicker * cryptography functions - the default JavaScript is not that speedy. * * Note that if you're in a node environment, this library will use * C++ code with node-gyp, so it will be nearly as fast as C++ implementations. * You only need to worry about this in less conventional environments, * like react-native, or possibly the web. * * If you don't know what you're doing, * DO NOT TOUCH THIS - YOU WILL BREAK WALLET SYNCING * * Note you don't have to set the globalIndex properties on returned inputs. * We will fetch them from the daemon if needed. However, if you have them, * return them, to save us a daemon call. * * Your function should return an array of `[publicSpendKey, TransactionInput]`. * The public spend key is the corresponding subwallet that the transaction input * belongs to. * * Return an empty array if no inputs are found that belong to the user. * * Example: * ```javascript * wallet.setBlockOutputProcessFunc(mySuperSpeedyFunction); * ``` * * @param func The function to process block outputs. * @param func.block The block to be processed. * @param func.privateViewKey The private view key of this wallet container. * @param func.spendKeys An array of [publicSpendKey, privateSpendKey]. These are the spend keys of each subwallet. * @param func.isViewWallet Whether this wallet is a view only wallet or not. * @param func.processCoinbaseTransactions Whether you should process coinbase transactions or not. */ setBlockOutputProcessFunc(func) { Logger_1.logger.log('Function setBlockOutputProcessFunc called', Logger_1.LogLevel.DEBUG, Logger_1.LogCategory.GENERAL); this.externalBlockProcessFunction = func; } /** * Initializes and starts the wallet sync process. You should call this * function before enquiring about daemon info or fee info. The wallet will * not process blocks until you call this method. * * Example: * ```javascript * await wallet.start(); * ``` */ start() { return __awaiter(this, void 0, void 0, function* () { Logger_1.logger.log('Function start called', Logger_1.LogLevel.DEBUG, Logger_1.LogCategory.GENERAL); if (!this.started) { this.started = true; yield this.daemon.init(); yield Promise.all([ this.syncThread.start(), this.daemonUpdateThread.start(), this.lockedTransactionsCheckThread.start() ]); } }); } /** * The inverse of the [[start]] method, this pauses the blockchain sync * process. * * If you want the node process to close cleanly (i.e, without using `process.exit()`), * you need to call this function. Otherwise, the library will keep firing * callbacks, and so your script will hang. * * Example: * ```javascript * wallet.stop(); * ``` */ stop() { return __awaiter(this, void 0, void 0, function* () { Logger_1.logger.log('Function stop called', Logger_1.LogLevel.DEBUG, Logger_1.LogCategory.GENERAL); this.started = false; yield this.syncThread.stop(); yield this.daemonUpdateThread.stop(); yield this.lockedTransactionsCheckThread.stop(); }); } /** * Get the node fee the daemon you are connected to is charging for * transactions. If the daemon charges no fee, this will return `['', 0]` * * Fees returned will be zero if you have not yet awaited [[start]]. * * Ex