turtlecoin-wallet-backend
Version:
[](https://npmjs.org/package/turtlecoin-wallet-backend)
1,126 lines • 102 kB
JavaScript
"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