turtlecoin-wallet-backend
Version:
[](https://travis-ci.org/turtlecoin/turtlecoin-wallet-backend-js)
1,213 lines • 65.7 kB
JavaScript
"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