@dashevo/wallet-lib
Version:
Light wallet library for Dash
440 lines (382 loc) • 14.9 kB
JavaScript
const _ = require('lodash');
const EventEmitter = require('events');
const logger = require('../../logger');
const { WALLET_TYPES, BIP44_ADDRESS_GAP } = require('../../CONSTANTS');
const { is } = require('../../utils');
const EVENTS = require('../../EVENTS');
const Wallet = require('../Wallet/Wallet');
const { simpleDescendingAccumulator } = require('../../utils/coinSelections/strategies');
const {
TxMetadataTimeoutError,
InstantLockTimeoutError,
} = require('../../errors');
function getNextUnusedAccountIndexForWallet(wallet) {
if (wallet && wallet.accounts) {
if (!wallet.accounts.length) return 0;
const indexes = wallet.accounts.reduce((acc, curr) => {
acc.push(curr.index);
return acc;
}, []).sort();
let index;
for (let i = 0; i <= indexes[indexes.length - 1] + 1; i += 1) {
if (!indexes.includes(i)) {
index = i;
break;
}
}
return index;
}
throw new Error('An account is attached to a wallet that has not been provided to the account constructor.');
}
const defaultOptions = {
network: 'testnet',
cacheTx: true,
cacheBlockHeaders: true,
allowSensitiveOperations: false,
plugins: [],
injectDefaultPlugins: true,
debug: false,
strategy: simpleDescendingAccumulator,
};
/* eslint-disable no-underscore-dangle */
const _initializeAccount = require('./_initializeAccount');
const _addAccountToWallet = require('./_addAccountToWallet');
const _loadStrategy = require('./_loadStrategy');
const getNetwork = require('./_getNetwork');
const getBIP44Path = require('./_getBIP44Path');
class Account extends EventEmitter {
constructor(wallet, opts = defaultOptions) {
super();
if (!wallet || wallet.constructor.name !== Wallet.name) throw new Error('Expected wallet to be passed as param');
if (!_.has(wallet, 'walletId')) throw new Error('Missing walletID to create an account');
this.walletId = wallet.walletId;
this.wallet = wallet;
this.logger = logger.getForWallet(this.walletId);
this.logger.debug(`Loading up wallet ${this.walletId}`);
this.identities = wallet.identities;
this.state = {
isInitialized: false,
isReady: false,
isDisconnecting: false,
};
this.injectDefaultPlugins = _.has(opts, 'injectDefaultPlugins') ? opts.injectDefaultPlugins : defaultOptions.injectDefaultPlugins;
this.allowSensitiveOperations = _.has(opts, 'allowSensitiveOperations') ? opts.allowSensitiveOperations : defaultOptions.allowSensitiveOperations;
this.debug = _.has(opts, 'debug') ? opts.debug : defaultOptions.debug;
// if (this.debug) process.env.LOG_LEVEL = 'debug';
this.waitForInstantLockTimeout = wallet.waitForInstantLockTimeout;
this.waitForTxMetadataTimeout = wallet.waitForTxMetadataTimeout;
this.walletType = wallet.walletType;
this.offlineMode = wallet.offlineMode;
this.index = _.has(opts, 'index') ? opts.index : getNextUnusedAccountIndexForWallet(wallet);
this.strategy = _loadStrategy(_.has(opts, 'strategy') ? opts.strategy : defaultOptions.strategy);
this.network = getNetwork(wallet.network).toString();
this.BIP44PATH = getBIP44Path(this.network, this.index);
this.transactions = {};
this.label = (opts && opts.label && is.string(opts.label)) ? opts.label : null;
// Forward async error events to wallet allowing catching during initial sync
this.on('error', (error, errorContext) => wallet.emit('error', error, {
...errorContext,
accountIndex: this.index,
network: this.network,
label: this.label,
}));
// If transport is null or invalid, we won't try to fetch anything
this.transport = wallet.transport;
this.storage = wallet.storage;
// Forward all storage event
this.storage.on(EVENTS.CONFIGURED, (ev) => this.emit(ev.type, ev));
this.storage.on(EVENTS.REHYDRATE_STATE_FAILED, (ev) => this.emit(ev.type, ev));
this.storage.on(EVENTS.REHYDRATE_STATE_SUCCESS, (ev) => this.emit(ev.type, ev));
this.storage.on(EVENTS.FETCHED_CONFIRMED_TRANSACTION, (ev) => this.emit(ev.type, ev));
this.storage.on(EVENTS.UNCONFIRMED_BALANCE_CHANGED, (ev) => this.emit(ev.type, ev));
this.storage.on(EVENTS.CONFIRMED_BALANCE_CHANGED, (ev) => this.emit(ev.type, ev));
this.storage.on(EVENTS.TX_METADATA, (ev) => {
this.emit(`${ev.type}:${ev.payload.hash}`, ev.payload.metadata);
});
this.storage.on(EVENTS.BLOCKHEADER, (ev) => this.emit(ev.type, ev));
this.on(
EVENTS.HEADERS_SYNC_PROGRESS,
(data) => wallet.emit(EVENTS.HEADERS_SYNC_PROGRESS, data),
);
this.on(
EVENTS.TRANSACTIONS_SYNC_PROGRESS,
(data) => wallet.emit(EVENTS.TRANSACTIONS_SYNC_PROGRESS, data),
);
this.on(
EVENTS.CONFIRMED_TRANSACTION,
(data) => wallet.emit(EVENTS.CONFIRMED_TRANSACTION, data),
);
this.on(
EVENTS.BLOCKHEIGHT_CHANGED,
(data) => wallet.emit(EVENTS.BLOCKHEIGHT_CHANGED, data),
);
if (this.debug) {
this.emit = (...args) => {
const { type } = args[1];
const payload = JSON.stringify(args[1].payload);
this.logger.debug(`${this.walletId}:${this.index} - Emitted event ${type} - ${payload} `);
super.emit(...args);
};
}
switch (this.walletType) {
case WALLET_TYPES.HDWALLET:
this.accountPath = getBIP44Path(this.network, this.index);
break;
case WALLET_TYPES.HDPUBLIC:
case WALLET_TYPES.PRIVATEKEY:
case WALLET_TYPES.PUBLICKEY:
case WALLET_TYPES.ADDRESS:
case WALLET_TYPES.SINGLE_ADDRESS:
this.accountPath = 'm/0';
break;
default:
throw new Error(`Invalid wallet type ${this.walletType}`);
}
this.storage
.getWalletStore(this.walletId)
.createPathState(this.accountPath);
let keyChainStorePath = this.index;
const keyChainStoreOpts = {};
switch (this.walletType) {
case WALLET_TYPES.HDPUBLIC:
keyChainStorePath = this.accountPath;
keyChainStoreOpts.lookAheadOpts = {
paths: {
'm/0': BIP44_ADDRESS_GAP,
},
};
break;
case WALLET_TYPES.HDWALLET:
case WALLET_TYPES.HDPRIVATE:
keyChainStorePath = this.BIP44PATH;
keyChainStoreOpts.lookAheadOpts = {
paths: {
'm/0': BIP44_ADDRESS_GAP,
'm/1': BIP44_ADDRESS_GAP,
},
};
break;
default:
break;
}
this.keyChainStore = wallet
.keyChainStore
.makeChildKeyChainStore(keyChainStorePath, keyChainStoreOpts);
// This forces keychainStore to set to issued key what is already its masterkey
if ([WALLET_TYPES.PUBLICKEY, WALLET_TYPES.PRIVATEKEY].includes(this.walletType)) {
this.keyChainStore
.getMasterKeyChain()
.getForPath('0', { isWatched: true });
}
this.cacheTx = (opts.cacheTx) ? opts.cacheTx : defaultOptions.cacheTx;
this.cacheBlockHeaders = (opts.cacheBlockHeaders)
? opts.cacheBlockHeaders
: defaultOptions.cacheBlockHeaders;
this.plugins = {
workers: {},
standard: {},
watchers: {},
};
this.emit(EVENTS.CREATED, { type: EVENTS.CREATED, payload: null });
/**
* Stores promise that waits for the transaction FETCH event
* @type {Promise<void>}
*/
this.txFetchListener = null;
this.broadcastRetryAttempts = 0;
// Increases a limit of max listeners for transactions related events
// 25 - mempool limit
this.setMaxListeners(25);
}
static getInstantLockTopicName(transactionHash) {
return `${EVENTS.INSTANT_LOCK}:${transactionHash}`;
}
async init(wallet) {
if (this.state.isInitialized) {
return true;
}
await _addAccountToWallet(this, wallet);
await _initializeAccount(this, wallet ? wallet.plugins : this.wallet.plugins);
return true;
}
async isInitialized() {
// eslint-disable-next-line consistent-return
return new Promise(((resolve) => {
if (this.state.isInitialized) {
resolve(true);
} else {
this.on(EVENTS.INITIALIZED, () => {
resolve(true);
});
}
}));
}
async isReady() {
// eslint-disable-next-line consistent-return
return new Promise(((resolve) => {
if (this.state.isReady) {
resolve(true);
} else {
this.on(EVENTS.READY, () => {
resolve(true);
});
}
}));
}
/**
* Imports instant lock to an account and emits message
* @param {InstantLock} instantLock
*/
importInstantLock(instantLock) {
const chainStore = this.storage.getChainStore(this.network);
chainStore.importInstantLock(instantLock);
this.emit(Account.getInstantLockTopicName(instantLock.txid), instantLock);
}
/**
* @param {string} transactionHash
* @param {function} callback
*/
subscribeToTransactionInstantLock(transactionHash, callback) {
const eventName = Account.getInstantLockTopicName(transactionHash);
this.once(eventName, callback);
return () => {
this.removeListener(eventName, callback);
};
}
/**
* @param {string} transactionHash
* @param {function} callback
* @returns {function} - cancel subscription
*/
subscribeToTxMetadata(transactionHash, callback) {
const eventName = `${EVENTS.TX_METADATA}:${transactionHash}`;
this.once(eventName, callback);
return () => {
this.removeListener(eventName, callback);
};
}
/**
* Waits for instant lock for a transaction or throws after a timeout
* @param {string} transactionHash - instant lock to wait for
* @param {number} timeout - in milliseconds before throwing an error if the lock didn't arrive
* @return {{promise: Promise<InstantLock>, cancel: Function}}
*/
waitForInstantLock(transactionHash, timeout = this.waitForInstantLockTimeout) {
// Return instant lock immediately if already exists
const chainStore = this.storage.getChainStore(this.network);
const instantLock = chainStore.getInstantLock(transactionHash);
if (instantLock != null) {
return {
promise: Promise.resolve(instantLock),
cancel: () => {
},
};
}
let rejectTimeout;
let cancelSubscription;
function cancel() {
cancelSubscription();
clearTimeout(rejectTimeout);
}
// Wait for upcoming instant lock
const promise = Promise.race([
new Promise((resolve) => {
cancelSubscription = this.subscribeToTransactionInstantLock(
transactionHash,
(instantLockData) => {
clearTimeout(rejectTimeout);
resolve(instantLockData);
},
);
}),
new Promise((resolve, reject) => {
rejectTimeout = setTimeout(() => {
cancelSubscription();
reject(new InstantLockTimeoutError(transactionHash));
}, timeout);
}),
]);
return {
promise,
cancel,
};
}
/**
* Waits for metadata of a transaction or throws an error after a timeout
* @param {string} transactionHash - metadata of tx to wait for
* @param {number} timeout - in ms before throwing an error if the metadata didn't arrive
* @return {{promise: Promise<InstantLock>, cancel: Function}}
*/
waitForTxMetadata(transactionHash, timeout = this.waitForTxMetadataTimeout) {
// Return tx metadata immediately if already exists
const chainStore = this.storage.getChainStore(this.network);
const txWithMetadata = chainStore.getTransaction(transactionHash);
if (txWithMetadata && txWithMetadata.metadata && txWithMetadata.metadata.height) {
return {
promise: Promise.resolve(txWithMetadata.metadata),
cancel: () => {
},
};
}
// Wait for upcoming metadata
let rejectTimeout;
let cancelSubscription;
function cancel() {
cancelSubscription();
clearTimeout(rejectTimeout);
}
const promise = Promise.race([
new Promise((resolve) => {
cancelSubscription = this.subscribeToTxMetadata(transactionHash, (metadata) => {
clearTimeout(rejectTimeout);
resolve(metadata);
});
}),
new Promise((resolve, reject) => {
rejectTimeout = setTimeout(() => {
cancelSubscription();
reject(new TxMetadataTimeoutError(transactionHash));
}, timeout);
}),
]);
return {
promise,
cancel,
};
}
}
Account.prototype.broadcastTransaction = require('./methods/broadcastTransaction');
Account.prototype.connect = require('./methods/connect');
Account.prototype.createTransaction = require('./methods/createTransaction');
Account.prototype.decode = require('./methods/decode');
Account.prototype.decrypt = require('./methods/decrypt');
Account.prototype.disconnect = require('./methods/disconnect');
Account.prototype.encode = require('./methods/encode');
Account.prototype.encrypt = require('./methods/encrypt');
Account.prototype.fetchStatus = require('./methods/fetchStatus');
Account.prototype.forceRefreshAccount = require('./methods/forceRefreshAccount');
Account.prototype.generateAddress = require('./methods/generateAddress');
Account.prototype.getAddress = require('./methods/getAddress');
Account.prototype.getAddresses = require('./methods/getAddresses');
Account.prototype.getBlockHeader = require('./methods/getBlockHeader');
Account.prototype.getConfirmedBalance = require('./methods/getConfirmedBalance');
Account.prototype.getPlugin = require('./methods/getPlugin');
Account.prototype.getPrivateKeys = require('./methods/getPrivateKeys');
Account.prototype.getTotalBalance = require('./methods/getTotalBalance');
Account.prototype.getTransaction = require('./methods/getTransaction');
Account.prototype.getTransactionHistory = require('./methods/getTransactionHistory');
Account.prototype.getTransactions = require('./methods/getTransactions');
Account.prototype.getUnconfirmedBalance = require('./methods/getUnconfirmedBalance');
Account.prototype.getUnusedAddress = require('./methods/getUnusedAddress');
Account.prototype.getUnusedIdentityIndex = require('./methods/getUnusedIdentityIndex');
Account.prototype.getUTXOS = require('./methods/getUTXOS');
Account.prototype.getWorker = require('./methods/getWorker');
Account.prototype.hasPlugins = require('./methods/hasPlugins');
Account.prototype.injectPlugin = require('./methods/injectPlugin');
Account.prototype.importTransactions = require('./methods/importTransactions');
Account.prototype.createPathsForTransactions = require('./methods/createPathsForTransactions');
Account.prototype.generateNewPaths = require('./methods/generateNewPaths');
Account.prototype.addPathsToStore = require('./methods/addPathsToStore');
Account.prototype.addDefaultPaths = require('./methods/addDefaultPaths');
Account.prototype.sign = require('./methods/sign');
module.exports = Account;