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)

1,213 lines 65.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 events_1 = require("events"); 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 CnUtils_1 = require("./CnUtils"); const ValidateParameters_1 = require("./ValidateParameters"); const WalletSynchronizer_1 = require("./WalletSynchronizer"); const Config_1 = require("./Config"); const Logger_1 = require("./Logger"); const WalletError_1 = require("./WalletError"); 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 { /** * @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 privateSpendKey Omit this parameter to create a view wallet. * */ constructor(config, daemon, address, scanHeight, newWallet, privateViewKey, privateSpendKey) { 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 = false; /** * 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; this.config = config; daemon.updateConfig(config); this.subWallets = new SubWallets_1.SubWallets(config, address, scanHeight, newWallet, privateViewKey, privateSpendKey); let timestamp = 0; if (newWallet) { timestamp = Utilities_1.getCurrentTimestampAdjusted(this.config.blockTargetTime); } this.daemon = daemon; this.walletSynchronizer = new WalletSynchronizer_1.WalletSynchronizer(daemon, this.subWallets, timestamp, scanHeight, privateViewKey, this.config); 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] = WB.WalletBackend.openWalletFromFile(daemon, 'mywallet.wallet', 'hunter2'); * * if (err) { * console.log('Failed to open wallet: ' + err.toString()); * } * ``` * @param filename The location of the wallet file on disk * * @param password The password to use to decrypt the wallet. May be blank. */ static openWalletFromFile(daemon, filename, password, config) { 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] = WB.WalletBackend.openWalletFromEncryptedString(daemon, data, 'hunter2'); * * if (err) { * console.log('Failed to open wallet: ' + err.toString()); * } * ``` * @param data The encrypted string representing the wallet data * * @param password The password to use to decrypt the wallet. May be blank. */ static openWalletFromEncryptedString(deamon, data, password, config) { 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(deamon, 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] = WB.WalletBackend.loadWalletFromJSON(daemon, json); * * if (err) { * console.log('Failed to load wallet: ' + err.toString()); * } * ``` * * @param daemon An implementation of the IDaemon interface. * * @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. */ static loadWalletFromJSON(daemon, json, config) { Assert_1.assertString(json, 'json'); try { const wallet = JSON.parse(json, WalletBackend.reviver); wallet.initAfterLoad(daemon, Config_1.MergeConfig(config)); 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] = WB.WalletBackend.importWalletFromSeed(daemon, 100000, seed); * * if (err) { * console.log('Failed to load wallet: ' + err.toString()); * } * ``` * * @param daemon An implementation of the IDaemon interface. * * @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. */ static importWalletFromSeed(daemon, scanHeight = 0, mnemonicSeed, config) { Assert_1.assertNumber(scanHeight, 'scanHeight'); Assert_1.assertString(mnemonicSeed, 'mnemonicSeed'); let keys; try { keys = CnUtils_1.CryptoUtils(Config_1.MergeConfig(config)).createAddressFromMnemonic(mnemonicSeed); } 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 = new WalletBackend(Config_1.MergeConfig(config), daemon, 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] = WB.WalletBackend.importWalletFromSeed(daemon, 100000, privateViewKey, privateSpendKey); * * if (err) { * console.log('Failed to load wallet: ' + err.toString()); * } * ``` * * @param daemon An implementation of the IDaemon interface. * * @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. */ static importWalletFromKeys(daemon, scanHeight = 0, privateViewKey, privateSpendKey, config) { 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)]; } let keys; try { keys = CnUtils_1.CryptoUtils(Config_1.MergeConfig(config)).createAddressFromKeys(privateSpendKey, privateViewKey); } 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 = new WalletBackend(Config_1.MergeConfig(config), daemon, keys.address, scanHeight, newWallet, keys.view.privateKey, keys.spend.privateKey); 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] = WB.WalletBackend.importViewWallet(daemon, 100000, privateViewKey, address); * * if (err) { * console.log('Failed to load wallet: ' + err.toString()); * } * ``` * * @param daemon An implementation of the IDaemon interface. * * @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. */ static importViewWallet(daemon, scanHeight = 0, privateViewKey, address, config) { 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 = 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 = new WalletBackend(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 = WB.WalletBackend.createWallet(daemon); * ``` * * @param daemon An implementation of the IDaemon interface. */ static createWallet(daemon, config) { const newWallet = true; const scanHeight = 0; const keys = CnUtils_1.CryptoUtils(Config_1.MergeConfig(config)).createNewAddress(); const wallet = new WalletBackend(Config_1.MergeConfig(config), daemon, keys.address, scanHeight, newWallet, keys.view.privateKey, keys.spend.privateKey); return wallet; } /* 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), }); } /** * 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* () { 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.walletSynchronizer.swapNode(newDaemon); 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. If you are using the [[Daemon]] daemon type, then * 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 it's specifics. * * Example: * ```javascript * const daemonInfo = wallet.getDaemonConnectionInfo(); * console.log(`Connected to ${daemonInfo.ssl ? 'https://' : 'http://'}${daemonInfo.host}:${daemonInfo.port}`); * ``` */ getDaemonConnectionInfo() { 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() { 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 timestamp The timestamp to being scanning transactions from */ reset(scanHeight = 0, scanTimestamp = 0) { return __awaiter(this, void 0, void 0, function* () { 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* () { 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()); }); } /** * Gets the wallet, local daemon, and network block count * * Example: * ```javascript * const [walletBlockCount, localDaemonBlockCount, networkBlockCount] = * wallet.getSyncStatus(); * ``` */ getSyncStatus() { 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() { 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) { Assert_1.assertBoolean(shouldScan, 'shouldScan'); this.config.scanCoinbaseTransactions = shouldScan; } /** * 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.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) { Assert_1.assertBoolean(shouldAutoOptimize, 'shouldAutoOptimize'); this.autoOptimize = shouldAutoOptimize; } /** * 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.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) { 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* () { if (!this.started) { this.started = true; yield this.daemon.init(); 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* () { 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]]. * * Example: * ```javascript * const [nodeFeeAddress, nodeFeeAmount] = wallet.getNodeFee(); * * if (nodeFeeAmount === 0) { * console.log('Yay, no fees!'); * } * ``` */ getNodeFee() { return this.daemon.nodeFee(); } /** * Gets the shared private view key for this wallet container. * * Example: * ```javascript * const privateViewKey = wallet.getPrivateViewKey(); * ``` */ getPrivateViewKey() { return this.subWallets.getPrivateViewKey(); } /** * Exposes some internal functions for those who know what they're doing... * * Example: * ```javascript * const syncFunc = wallet.internal().sync; * await syncFunc(true); * ``` * * @returns Returns an object with two members, sync(), and updateDaemonInfo(). */ internal() { return { sync: (sleep) => this.sync(sleep), updateDaemonInfo: () => this.updateDaemonInfo(), }; } /** * Gets the publicSpendKey and privateSpendKey for the given address, if * possible. * * Note: secret key will be 00000... (64 zeros) if this wallet is a view only wallet. * * Example: * ```javascript * const [publicSpendKey, privateSpendKey, err] = wallet.getSpendKeys('TRTLxyz...'); * * if (err) { * console.log('Failed to get spend keys for address: ' + err.toString()); * } * ``` * * @param address A valid address in this container, to get the spend keys of */ getSpendKeys(address) { Assert_1.assertString(address, 'address'); const integratedAddressesAllowed = false; const err = ValidateParameters_1.validateAddresses(new Array(address), integratedAddressesAllowed, this.config); if (!_.isEqual(err, WalletError_1.SUCCESS)) { return [undefined, undefined, err]; } const [publicViewKey, publicSpendKey] = Utilities_1.addressToKeys(address, this.config); const [err2, privateSpendKey] = this.subWallets.getPrivateSpendKey(publicSpendKey); if (!_.isEqual(err2, WalletError_1.SUCCESS)) { return [undefined, undefined, err2]; } return [publicSpendKey, privateSpendKey, undefined]; } /** * Gets the private spend and private view for the primary address. * The primary address is the first created wallet in the container. * * Example: * ```javascript * const [privateSpendKey, privateViewKey] = wallet.getPrimaryAddressPrivateKeys(); * ``` */ getPrimaryAddressPrivateKeys() { return [this.subWallets.getPrimaryPrivateSpendKey(), this.getPrivateViewKey()]; } /** * Get the primary address mnemonic seed. If the primary address isn't * a deterministic wallet, it will return a WalletError. * * Example: * ```javascript * const [seed, err] = wallet.getMnemonicSeed(); * * if (err) { * console.log('Wallet is not a deterministic wallet: ' + err.toString()); * } * ``` */ getMnemonicSeed() { return this.getMnemonicSeedForAddress(this.subWallets.getPrimaryAddress()); } /** * Get the mnemonic seed for the specified address. If the specified address * is invalid or the address isn't a deterministic wallet, it will return * a WalletError. * * Example: * ```javascript * const [seed, err] = wallet.getMnemonicSeedForAddress('TRTLxyz...'); * * if (err) { * console.log('Address does not belong to a deterministic wallet: ' + err.toString()); * } * ``` * * @param address A valid address that exists in this container */ getMnemonicSeedForAddress(address) { Assert_1.assertString(address, 'address'); const privateViewKey = this.getPrivateViewKey(); const [publicSpendKey, privateSpendKey, error] = this.getSpendKeys(address); if (error) { return [undefined, error]; } const parsedAddr = CnUtils_1.CryptoUtils(this.config).createAddressFromKeys(privateSpendKey, privateViewKey); if (!parsedAddr.mnemonic) { return [undefined, new WalletError_1.WalletError(WalletError_1.WalletErrorCode.KEYS_NOT_DETERMINISTIC)]; } return [parsedAddr.mnemonic, undefined]; } /** * Gets the primary address of a wallet container. * The primary address is the address that was created first in the wallet * container. * * Example: * ```javascript * const address = wallet.getPrimaryAddress(); * ``` */ getPrimaryAddress() { return this.subWallets.getPrimaryAddress(); } /** * Encrypt the wallet using the given password. Password may be empty. Note that an empty password does not mean an * unencrypted wallet - simply a wallet encrypted with the empty string. * * This will take some time (Roughly a second on a modern PC) - it runs 500,000 iterations of pbkdf2. * * Example: * ```javascript * const saved = wallet.encryptWalletToString('hunter2'); * * ``` * * @param password The password to encrypt the wallet with * * @return Returns the encrypted wallet as astring. */ encryptWalletToString(password) { Assert_1.assertString(password, 'password'); const walletJson = JSON.stringify(this); return WalletEncryption_1.WalletEncryption.encryptWalletToString(walletJson, password); } /** * Save the wallet to the given filename. Password may be empty, but * filename must not be. Note that an empty password does not mean an * unencrypted wallet - simply a wallet encrypted with the empty string. * * This will take some time (Roughly a second on a modern PC) - it runs 500,000 iterations of pbkdf2. * * Example: * ```javascript * const saved = wallet.saveWalletToFile('test.wallet', 'hunter2'); * * if (!saved) { * console.log('Failed to save wallet!'); * } * ``` * * @param filename The file location to save the wallet to. * @param password The password to encrypt the wallet with * * @return Returns a boolean indicating success. */ saveWalletToFile(filename, password) { Assert_1.assertString(filename, 'filename'); Assert_1.assertString(password, 'password'); const walletJson = JSON.stringify(this); const fileData = WalletEncryption_1.WalletEncryption.encryptWalletToBuffer(walletJson, password); try { fs.writeFileSync(filename, fileData); return true; } catch (err) { Logger_1.logger.log('Failed to write file: ' + err.toString(), Logger_1.LogLevel.ERROR, [Logger_1.LogCategory.FILESYSTEM, Logger_1.LogCategory.SAVE]); return false; } } /** * Gets the address of every subwallet in this container. * * Example: * ```javascript * let i = 1; * * for (const address of wallet.getAddresses()) { * console.log(`Address [${i}]: ${address}`); * i++; * } * ``` */ getAddresses() { return this.subWallets.getAddresses(); } /** * Optimizes your wallet as much as possible. It will optimize every single * subwallet correctly, if you have multiple subwallets. Note that this * method does not wait for the funds to return to your wallet before * returning, so, it is likely balances will remain locked. * * Note that if you want to alert the user in real time of the hashes or * number of transactions sent, you can subscribe to the `createdfusiontx` * event. This will be fired every time a fusion transaction is sent. * * You may also want to consider manually creating individual transactions * if you want more control over the process. See [[sendFusionTransactionBasic]]. * * This method may take a *very long time* if your wallet is not optimized * at all. It is suggested to not block the UI/mainloop of your program * when using this method. * * Example: * ```javascript * const [numberOfTransactionsSent, hashesOfSentFusionTransactions] = await wallet.optimize(); * * console.log(`Sent ${numberOfTransactionsSent} fusion transactions, hashes: ${hashesOfSentFusionTransactions.join(', ')}`); * ``` */ optimize() { return __awaiter(this, void 0, void 0, function* () { let numTransactionsSent = 0; let hashes = []; for (const address of this.getAddresses()) { const [numSent, newHashes] = yield this.optimizeAddress(address); numTransactionsSent += numSent; hashes = hashes.concat(newHashes); } return [numTransactionsSent, hashes]; }); } /** * Sends a fusion transaction, if possible. * Fusion transactions are zero fee, and optimize your wallet * for sending larger amounts. You may (probably will) need to perform * multiple fusion transactions. * * If you want to ensure your wallet gets fully optimized, consider using * [[optimize]]. * * Example: * ```javascript * const [hash, err] = await wallet.sendFusionTransactionBasic(); * * if (err) { * console.log('Failed to send fusion transaction: ' + err.toString()); * } * ``` */ sendFusionTransactionBasic() { return __awaiter(this, void 0, void 0, function* () { this.currentlyTransacting = true; const f = () => __awaiter(this, void 0, void 0, function* () { const [transaction, hash, error] = yield Transfer_1.sendFusionTransactionBasic(this.config, this.daemon, this.subWallets); if (transaction) { this.emit('createdfusiontx', transaction); } /* Typescript is too dumb for return [hash, error] to work.. */ if (hash) { Logger_1.logger.log('Sent fusion transaction ' + hash, Logger_1.LogLevel.INFO, Logger_1.LogCategory.TRANSACTIONS); return [hash, undefined]; } else { return [undefined, error]; } }); const result = yield f(); this.currentlyTransacting = false; return result; }); } /** * Sends a fusion transaction, if possible. * Fusion transactions are zero fee, and optimize your wallet * for sending larger amounts. You may (probably will) need to perform * multiple fusion transactions. * * If you want to ensure your wallet gets fully optimized, consider using * [[optimize]]. * * All parameters are optional. * * Example: * ```javascript * const [hash, err] = await wallet.sendFusionTransactionAdvanced(3, undefined, 'TRTLxyz..'); * * if (err) { * console.log('Failed to send transaction: ' + err.toString()); * } * ``` * * @param mixin The amount of input keys to hide your input with. * Your network may enforce a static mixin. * @param subWalletsToTakeFrom The addresses of the subwallets to draw funds from. * @param destination The destination for the fusion transaction to be sent to. * Must be an address existing in this container. */ sendFusionTransactionAdvanced(mixin, subWalletsToTakeFrom, destination) { return __awaiter(this, void 0, void 0, function* () { Assert_1.assertNumberOrUndefined(mixin, 'mixin'); Assert_1.assertArrayOrUndefined(subWalletsToTakeFrom, 'subWalletsToTakeFrom'); Assert_1.assertStringOrUndefined(destination, 'destination'); this.currentlyTransacting = true; const f = () => __awaiter(this, void 0, void 0, function* () { const [transaction, hash, error] = yield Transfer_1.sendFusionTransactionAdvanced(this.config, this.daemon, this.subWallets, mixin, subWalletsToTakeFrom, destination); if (transaction) { this.emit('createdfusiontx', transaction); } /* Typescript is too dumb for return [hash, error] to work.. */ if (hash) { Logger_1.logger.log('Sent fusion transaction ' + hash, Logger_1.LogLevel.INFO, Logger_1.LogCategory.TRANSACTIONS); return [hash, undefined]; } else { return [undefined, error]; } }); const result = yield f(); this.currentlyTransacting = false; return result; }); } /** * Sends a transaction of amount to the address destination, using the * given payment ID, if specified. * * Network fee is set to default, mixin is set to default, all subwallets * are taken from, primary address is used as change address. * * If you need more control, use [[sendTransactionAdvanced]]. * * Example: * ```javascript * const [hash, err] = await wallet.sendTransactionBasic('TRTLxyz...', 1234); * * if (err) { * console.log('Failed to send transaction: ' + err.toString()); * } * ``` * * @param destination The address to send the funds to * @param amount The amount to send, in ATOMIC units * @param paymentID The payment ID to include with this transaction. Optional. * * @return Returns either an error, or the transaction hash. */ sendTransactionBasic(destination, amount, paymentID) { return __awaiter(this, void 0, void 0, function* () { Assert_1.assertString(destination, 'destination'); Assert_1.assertNumber(amount, 'amount'); Assert_1.assertStringOrUndefined(paymentID, 'paymentID'); this.currentlyTransacting = true; const f = () => __awaiter(this, void 0, void 0, function* () { const [transaction, hash, error] = yield Transfer_1.sendTransactionBasic(this.config, this.daemon, this.subWallets, destination, amount, paymentID); if (transaction) { this.emit('createdtx', transaction); } /* Typescript is too dumb for return [hash, error] to work.. */ if (hash) { Logger_1.logger.log('Sent transaction ' + hash, Logger_1.LogLevel.INFO, Logger_1.LogCategory.TRANSACTIONS); return [hash, undefined]; } else { return [undefined, error]; } }); const result = yield f(); this.currentlyTransacting = false; return result; }); } /** * Sends a transaction, which permits multiple amounts to different destinations, * specifying the mixin, fee, subwallets to draw funds from, and change address. * * All parameters are optional aside from destinations. * * Example: * ```javascript * const destinations = [ * ['TRTLxyz...', 1000], * ['TRTLzyx...', 10000], * ]; * * const [hash, err] = await wallet.sendTransactionAdvanced(destinations, undefined, 100, 'c59d157d1d96f280ece0816a8925cae8232432b7235d1fa92c70faf3064434b3'); * * if (err) { * console.log('Failed to send transaction: ' + err.toString()); * } * ``` * * @param destinations An array of destinations, and amounts to send to that * destination. Amounts are in ATOMIC units. * @param mixin The amount of input keys to hide your input with. * Your network may enforce a static mixin. * @param fee The network fee to use with this transaction. In ATOMIC units. * @param paymentID The payment ID to include with this transaction. Defaults to none. * @param subWalletsToTakeFrom The addresses of the subwallets to draw funds from. Defaults to all addresses. * @param changeAddress The address to send any returned change to. Defaults to the primary address. */ sendTransactionAdvanced(destinations, mixin, fee, paymentID, subWalletsToTakeFrom, changeAddress) { return __awaiter(this, void 0, void 0, function* () { Assert_1.assertArray(destinations, 'destinations'); Assert_1.assertNumberOrUndefined(mixin, 'mixin'); Assert_1.assertNumberOrUndefined(fee, 'fee'); Assert_1.assertStringOrUndefined(paymentID, 'paymentID'); Assert_1.assertArrayOrUndefined(subWalletsToTakeFrom, 'subWalletsToTakeFrom'); Assert_1.assertStringOrUndefined(changeAddress, 'changeAddress'); this.currentlyTransacting = true; const f = () => __awaiter(this, void 0, void 0, function* () { const [transaction, hash, error] = yield Transfer_1.sendTransactionAdvanced(this.config, this.daemon, this.subWallets, destinations, mixin, fee, paymentID, subWalletsToTakeFrom, changeAddress); if (transaction) { this.emit('createdtx', transaction); } /* Typescript is too dumb for return [hash, error] to work.. */ if (hash) { Logger_1.logger.log('Sent transaction ' + hash, Logger_1.LogLevel.INFO, Logger_1.LogCategory.TRANSACTIONS); return [hash, undefined]; } else { return [undefined, error]; } }); const result = yield f(); this.currentlyTransacting = false; return result; }); } /** * Get the unlocked and locked balance for the wallet container. * * Example: * ```javascript * const [unlockedBalance, lockedBalance] = wallet.getBalance(); * ``` * * @param subWalletsToTakeFrom The addresses to check the balance of. If * not given, defaults to all addresses. */ getBalance(subWalletsToTakeFrom) { Assert_1.assertArrayOrUndefined(subWalletsToTakeFrom, 'subWalletsToTakeFrom'); return this.subWallets.getBalance(this.daemon.getNetworkBlockCount(), subWalletsToTakeFrom); } /** * Gets all the transactions in the wallet container. * * Newer transactions are at the front of the array - Unconfirmed transactions * come at the very front. * * Example: * ```javascript * for (const tx of wallet.getTransactions()) { * console.log(`Transaction ${tx.hash} - ${WB.prettyPrintAmount(tx.totalAmount())} - ${tx.timestamp}`); * } * ``` * * @param startIndex Index to start taking transactions from * @param numTransactions Number of transactions to take * @param includeFusions Should we include fusion transactions? */ getTransactions(startIndex, numTransactions, includeFusions = true) { Assert_1.assertNumberOrUndefined(startIndex, 'startIndex'); Assert_1.assertNumberOrUndefined(numTransactions, 'numTransactions'); Assert_1.assertBoolean(includeFusions, 'includeFusions'); /* Clone the array and reverse it, newer txs first */ const unconfirmed = this.subWallets.getUnconfirmedTransactions().slice().reverse(); /* Clone the array and reverse it, newer txs first */ const confirmed = this.subWallets.getTransactions().slic