copay-crown
Version:
A Secure Crown Wallet
1,345 lines (1,145 loc) • 44.6 kB
text/typescript
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Events } from 'ionic-angular';
import * as lodash from 'lodash';
import { Logger } from '../../providers/logger/logger';
// Providers
import { BwcErrorProvider } from '../bwc-error/bwc-error';
import { BwcProvider } from '../bwc/bwc';
import { ConfigProvider } from '../config/config';
import { FeeProvider } from '../fee/fee';
import { FilterProvider } from '../filter/filter';
import { OnGoingProcessProvider } from '../on-going-process/on-going-process';
import { PersistenceProvider } from '../persistence/persistence';
import { PopupProvider } from '../popup/popup';
import { RateProvider } from '../rate/rate';
import { TouchIdProvider } from '../touchid/touchid';
import { TxFormatProvider } from '../tx-format/tx-format';
export class WalletProvider {
// Ratio low amount warning (fee/amount) in incoming TX
private LOW_AMOUNT_RATIO: number = 0.15;
// Ratio of "many utxos" warning in total balance (fee/amount)
private TOTAL_LOW_WARNING_RATIO: number = .3;
private WALLET_STATUS_MAX_TRIES: number = 7;
private WALLET_STATUS_DELAY_BETWEEN_TRIES: number = 1.4 * 1000;
private SOFT_CONFIRMATION_LIMIT: number = 12;
private SAFE_CONFIRMATIONS: number = 6;
private errors: any = this.bwcProvider.getErrors();
private progressFn = {};
/* TODO: update on progress
private updateOnProgress = {}
*/
constructor(
private logger: Logger,
private bwcProvider: BwcProvider,
private txFormatProvider: TxFormatProvider,
private configProvider: ConfigProvider,
private persistenceProvider: PersistenceProvider,
private bwcErrorProvider: BwcErrorProvider,
private rateProvider: RateProvider,
private filter: FilterProvider,
private popupProvider: PopupProvider,
private onGoingProcessProvider: OnGoingProcessProvider,
private touchidProvider: TouchIdProvider,
private events: Events,
private feeProvider: FeeProvider,
private translate: TranslateService
) {
this.logger.info('WalletService initialized.');
}
private invalidateCache(wallet: any) {
if (wallet.cachedStatus)
wallet.cachedStatus.isValid = false;
if (wallet.completeHistory)
wallet.completeHistory.isValid = false;
if (wallet.cachedActivity)
wallet.cachedActivity.isValid = false;
if (wallet.cachedTxps)
wallet.cachedTxps.isValid = false;
}
public getStatus(wallet: any, opts: any): Promise<any> {
return new Promise((resolve, reject) => {
opts = opts || {};
var walletId = wallet.id;
let processPendingTxps = (status: any) => {
let txps = status.pendingTxps;
let now = Math.floor(Date.now() / 1000);
/* To test multiple outputs...
var txp = {
message: 'test multi-output',
fee: 1000,
createdOn: new Date() / 1000,
outputs: []
};
function addOutput(n) {
txp.outputs.push({
amount: 600,
toAddress: '2N8bhEwbKtMvR2jqMRcTCQqzHP6zXGToXcK',
message: 'output #' + (Number(n) + 1)
});
};
lodash.times(150, addOutput);
txps.push(txp);
*/
lodash.each(txps, (tx: any) => {
tx = this.txFormatProvider.processTx(wallet.coin, tx);
// no future transactions...
if (tx.createdOn > now)
tx.createdOn = now;
tx.wallet = wallet;
if (!tx.wallet) {
this.logger.error("no wallet at txp?");
return;
}
let action: any = lodash.find(tx.actions, {
copayerId: tx.wallet.copayerId
});
if (!action && tx.status == 'pending') {
tx.pendingForUs = true;
}
if (action && action.type == 'accept') {
tx.statusForUs = 'accepted';
} else if (action && action.type == 'reject') {
tx.statusForUs = 'rejected';
} else {
tx.statusForUs = 'pending';
}
if (!tx.deleteLockTime)
tx.canBeRemoved = true;
});
wallet.pendingTxps = txps;
};
let get = (): Promise<any> => {
return new Promise((resolve, reject) => {
wallet.getStatus({
twoStep: true
}, (err, ret) => {
if (err) {
if (err instanceof this.errors.NOT_AUTHORIZED) {
return reject('WALLET_NOT_REGISTERED');
}
return reject(err);
}
return resolve(ret);
});
})
};
let cacheBalance = (wallet: any, balance: any): void => {
if (!balance) return;
let configGet: any = this.configProvider.get();
let config: any = configGet.wallet;
let cache = wallet.cachedStatus;
// Address with Balance
cache.balanceByAddress = balance.byAddress;
// Total wallet balance is same regardless of 'spend unconfirmed funds' setting.
cache.totalBalanceSat = balance.totalAmount;
// Spend unconfirmed funds
if (config.spendUnconfirmed) {
cache.lockedBalanceSat = balance.lockedAmount;
cache.availableBalanceSat = balance.availableAmount;
cache.totalBytesToSendMax = balance.totalBytesToSendMax;
cache.pendingAmount = 0;
cache.spendableAmount = balance.totalAmount - balance.lockedAmount;
} else {
cache.lockedBalanceSat = balance.lockedConfirmedAmount;
cache.availableBalanceSat = balance.availableConfirmedAmount;
cache.totalBytesToSendMax = balance.totalBytesToSendConfirmedMax;
cache.pendingAmount = balance.totalAmount - balance.totalConfirmedAmount;
cache.spendableAmount = balance.totalConfirmedAmount - balance.lockedAmount;
}
// Selected unit
cache.unitToSatoshi = config.settings.unitToSatoshi;
cache.satToUnit = 1 / cache.unitToSatoshi;
// STR
cache.totalBalanceStr = this.txFormatProvider.formatAmountStr(wallet.coin, cache.totalBalanceSat);
cache.lockedBalanceStr = this.txFormatProvider.formatAmountStr(wallet.coin, cache.lockedBalanceSat);
cache.availableBalanceStr = this.txFormatProvider.formatAmountStr(wallet.coin, cache.availableBalanceSat);
cache.spendableBalanceStr = this.txFormatProvider.formatAmountStr(wallet.coin, cache.spendableAmount);
cache.pendingBalanceStr = this.txFormatProvider.formatAmountStr(wallet.coin, cache.pendingAmount);
cache.alternativeName = config.settings.alternativeName;
cache.alternativeIsoCode = config.settings.alternativeIsoCode;
// Check address
this.isAddressUsed(wallet, balance.byAddress).then((used) => {
if (used) {
this.logger.debug('Address used. Creating new');
// Force new address
this.getAddress(wallet, true).then((addr) => {
this.logger.debug('New address: ', addr);
}).catch((err) => {
return reject(err);
});
}
}).catch((err) => {
return reject(err);
});
this.rateProvider.whenRatesAvailable().then(() => {
let totalBalanceAlternative = this.rateProvider.toFiat(cache.totalBalanceSat, cache.alternativeIsoCode, wallet.coin);
let pendingBalanceAlternative = this.rateProvider.toFiat(cache.pendingAmount, cache.alternativeIsoCode, wallet.coin);
let lockedBalanceAlternative = this.rateProvider.toFiat(cache.lockedBalanceSat, cache.alternativeIsoCode, wallet.coin);
let spendableBalanceAlternative = this.rateProvider.toFiat(cache.spendableAmount, cache.alternativeIsoCode, wallet.coin);
let alternativeConversionRate = this.rateProvider.toFiat(100000000, cache.alternativeIsoCode, wallet.coin);
cache.totalBalanceAlternative = this.filter.formatFiatAmount(totalBalanceAlternative);
cache.pendingBalanceAlternative = this.filter.formatFiatAmount(pendingBalanceAlternative);
cache.lockedBalanceAlternative = this.filter.formatFiatAmount(lockedBalanceAlternative);
cache.spendableBalanceAlternative = this.filter.formatFiatAmount(spendableBalanceAlternative);
cache.alternativeConversionRate = this.filter.formatFiatAmount(alternativeConversionRate);
cache.alternativeBalanceAvailable = true;
cache.isRateAvailable = true;
}).catch((err) => {
this.logger.warn(err);
});
};
let isStatusCached = (): any => {
return wallet.cachedStatus && wallet.cachedStatus.isValid;
};
let cacheStatus = (status: any): void => {
if (status.wallet && status.wallet.scanStatus == 'running') return;
wallet.cachedStatus = status || {};
let cache = wallet.cachedStatus;
cache.statusUpdatedOn = Date.now();
cache.isValid = true;
cache.email = status.preferences ? status.preferences.email : null;
cacheBalance(wallet, status.balance);
};
let walletStatusHash = (status: any): any => {
return status ? status.balance.totalAmount : wallet.totalBalanceSat;
};
let _getStatus = (initStatusHash: any, tries: number): Promise<any> => {
return new Promise((resolve, reject) => {
if (isStatusCached() && !opts.force) {
this.logger.debug('Wallet status cache hit:' + wallet.id);
cacheStatus(wallet.cachedStatus);
processPendingTxps(wallet.cachedStatus);
return resolve(wallet.cachedStatus);
};
tries = tries || 0;
this.logger.debug('Updating Status:', wallet.credentials.walletName, tries);
get().then((status) => {
let currentStatusHash = walletStatusHash(status);
this.logger.debug('Status update. hash:' + currentStatusHash + ' Try:' + tries);
if (opts.untilItChanges && initStatusHash == currentStatusHash && tries < this.WALLET_STATUS_MAX_TRIES && walletId == wallet.credentials.walletId) {
return setTimeout(() => {
this.logger.debug('Retrying update... ' + walletId + ' Try:' + tries)
return _getStatus(initStatusHash, ++tries);
}, this.WALLET_STATUS_DELAY_BETWEEN_TRIES * tries);
}
processPendingTxps(status);
this.logger.debug('Got Wallet Status for:' + wallet.credentials.walletName);
cacheStatus(status);
wallet.scanning = status.wallet && status.wallet.scanStatus == 'running';
return resolve(status);
}).catch((err) => {
return reject(err);
});
});
};
_getStatus(walletStatusHash(null), 0).then((status) => {
resolve(status);
}).catch((err) => {
return reject(err);
});
});
}
// Check address
private isAddressUsed(wallet: any, byAddress: any[]): Promise<any> {
return new Promise((resolve, reject) => {
this.persistenceProvider.getLastAddress(wallet.id).then((addr) => {
let used = lodash.find(byAddress, {
address: addr
});
return resolve(used);
}).catch((err) => {
return reject(err);
});
});
}
public useLegacyAddress(): boolean {
let config = this.configProvider.get();
let walletSettings = config.wallet;
return walletSettings.useLegacyAddress;
}
public getAddressView(wallet: any, address: string): string {
if (wallet.coin != 'bch' || this.useLegacyAddress()) return address;
return this.txFormatProvider.toCashAddress(address);
}
public getProtoAddress(wallet: any, address: string) {
let proto: string = this.getProtocolHandler(wallet.coin);
let protoAddr: string = proto + ':' + address;
if (wallet.coin != 'bch' || this.useLegacyAddress()) {
return protoAddr;
} else {
return protoAddr.toUpperCase();
};
};
public getAddress(wallet: any, forceNew: boolean): Promise<any> {
return new Promise((resolve, reject) => {
this.persistenceProvider.getLastAddress(wallet.id).then((addr) => {
if (!forceNew && addr) return resolve(addr);
if (!wallet.isComplete())
return reject(this.bwcErrorProvider.msg('WALLET_NOT_COMPLETE'));
if (wallet.needsBackup) {
return reject(this.bwcErrorProvider.msg('WALLET_NEEDS_BACKUP'));
}
this.createAddress(wallet).then((_addr) => {
this.persistenceProvider.storeLastAddress(wallet.id, _addr).then(() => {
return resolve(_addr);
}).catch((err) => {
return reject(err);
});
}).catch((err) => {
return reject(err);
});
}).catch((err) => {
return reject(err);
});
});
}
private createAddress(wallet: any): Promise<any> {
return new Promise((resolve, reject) => {
this.logger.debug('Creating address for wallet:', wallet.id);
wallet.createAddress({}, (err, addr) => {
if (err) {
let prefix = this.translate.instant('Could not create address');
if (err instanceof this.errors.CONNECTION_ERROR || (err.message && err.message.match(/5../))) {
this.logger.warn(err);
return setTimeout(() => {
this.createAddress(wallet);
}, 5000);
} else if (
err instanceof this.errors.MAIN_ADDRESS_GAP_REACHED || (err.message && err.message == 'MAIN_ADDRESS_GAP_REACHED')
) {
this.logger.warn(this.bwcErrorProvider.msg(err, 'Server Error'));
prefix = null;
wallet.getMainAddresses({
reverse: true,
limit: 1
}, (err, addr) => {
if (err) return reject(err);
return resolve(addr[0].address);
});
} else {
this.bwcErrorProvider.cb(err, prefix).then((msg) => {
return reject(msg);
});
}
} else
return resolve(addr.address);
});
});
}
private getSavedTxs(walletId: string): Promise<any> {
return new Promise((resolve, reject) => {
this.persistenceProvider.getTxHistory(walletId).then((txs: any) => {
let localTxs = [];
if (lodash.isEmpty(txs)) {
return resolve(localTxs);
};
try {
localTxs = JSON.parse(txs);
} catch (ex) {
localTxs = txs;
};
return resolve(lodash.compact(localTxs));
}).catch((err: Error) => {
return reject(err);
});
});
}
private getTxsFromServer(wallet: any, skip: number, endingTxid: string, limit: number): Promise<any> {
return new Promise((resolve, reject) => {
let res = [];
let result = {
res,
shouldContinue: res.length >= limit
};
wallet.getTxHistory({
skip,
limit
}, (err: Error, txsFromServer: any[]) => {
if (err) return reject(err);
if (lodash.isEmpty(txsFromServer))
return resolve(result);
res = lodash.takeWhile(txsFromServer, (tx) => {
return tx.txid != endingTxid;
});
result.res = res;
result.shouldContinue = res.length >= limit;
return resolve(result);
});
});
}
private updateLocalTxHistory(wallet: any, opts: any): Promise<any> {
return new Promise((resolve, reject) => {
opts = opts ? opts : {};
let FIRST_LIMIT = 5;
let LIMIT = 50;
let requestLimit = FIRST_LIMIT;
let walletId = wallet.credentials.walletId;
this.progressFn[walletId] = opts.progressFn || (() => { });
let foundLimitTx = [];
let fixTxsUnit = (txs: any): void => {
if (!txs || !txs[0] || !txs[0].amountStr) return;
let cacheCoin: string = txs[0].amountStr.split(' ')[1];
if (cacheCoin == 'bits') {
this.logger.debug('Fixing Tx Cache Unit to: ' + wallet.coin)
lodash.each(txs, (tx: any) => {
tx.amountStr = this.txFormatProvider.formatAmountStr(wallet.coin, tx.amount);
tx.feeStr = this.txFormatProvider.formatAmountStr(wallet.coin, tx.fees);
});
};
};
/* TODO: update on progress
if (updateOnProgress[wallet.id]) {
$log.warn('History update already on progress for: '+ wallet.credentials.walletName);
if (opts.progressFn) {
$log.debug('Rewriting progressFn');
progressFn[walletId] = opts.progressFn;
}
updateOnProgress[wallet.id].push(cb);
return; // no callback call yet.
}
updateOnProgress[walletId] = [cb];
*/
this.getSavedTxs(walletId).then((txsFromLocal: any) => {
fixTxsUnit(txsFromLocal);
let confirmedTxs = this.removeAndMarkSoftConfirmedTx(txsFromLocal);
let endingTxid = confirmedTxs[0] ? confirmedTxs[0].txid : null;
let endingTs = confirmedTxs[0] ? confirmedTxs[0].time : null;
// First update
this.progressFn[walletId](txsFromLocal, 0);
wallet.completeHistory = txsFromLocal;
let getNewTxs = (newTxs: any[], skip: number): Promise<any> => {
return new Promise((resolve, reject) => {
this.getTxsFromServer(wallet, skip, endingTxid, requestLimit).then((result: any) => {
let res = result.res;
let shouldContinue = result.shouldContinue ? result.shouldContinue : false;
newTxs = newTxs.concat(this.processNewTxs(wallet, lodash.compact(res)));
this.progressFn[walletId](newTxs.concat(txsFromLocal), newTxs.length);
skip = skip + requestLimit;
this.logger.debug('Syncing TXs. Got:' + newTxs.length + ' Skip:' + skip, ' EndingTxid:', endingTxid, ' Continue:', shouldContinue);
// TODO Dirty <HACK>
// do not sync all history, just looking for a single TX.
if (opts.limitTx) {
foundLimitTx = lodash.find(newTxs, {
txid: opts.limitTx,
});
if (!lodash.isEmpty(foundLimitTx)) {
this.logger.debug('Found limitTX: ' + opts.limitTx);
return resolve([foundLimitTx]);
}
}
// </HACK>
if (!shouldContinue) {
this.logger.debug('Finished Sync: New / soft confirmed Txs: ' + newTxs.length);
return resolve(newTxs);
};
requestLimit = LIMIT;
getNewTxs(newTxs, skip).then((txs: any) => {
resolve(txs);
});
}).catch((err) => {
if (err instanceof this.errors.CONNECTION_ERROR || (err.message && err.message.match(/5../))) {
this.logger.info('Retrying history download in 5 secs...');
return reject(setTimeout(() => {
return getNewTxs(newTxs, skip);
}, 5000));
};
return reject(err);
});
});
};
getNewTxs([], 0).then((txs: any) => {
let array: any[] = lodash.compact(txs.concat(confirmedTxs));
let newHistory = lodash.uniqBy(array, (x: any) => {
return x.txid;
});
let updateNotes = (): Promise<any> => {
return new Promise((resolve, reject) => {
if (!endingTs) return resolve();
this.logger.debug('Syncing notes from: ' + endingTs);
wallet.getTxNotes({
minTs: endingTs
}, (err: any, notes: any) => {
if (err) {
this.logger.warn(err);
return reject(err);
};
lodash.each(notes, (note: any) => {
this.logger.debug('Note for ' + note.txid);
lodash.each(newHistory, (tx: any) => {
if (tx.txid == note.txid) {
this.logger.debug('...updating note for ' + note.txid);
tx.note = note;
};
});
});
return resolve();
});
});
};
let updateLowAmount = (txs: any) => {
if (!opts.lowAmount) return;
lodash.each(txs, (tx: any) => {
tx.lowAmount = tx.amount < opts.lowAmount;
});
};
this.getLowAmount(wallet).then((fee) => {
opts.lowAmount = fee;
updateLowAmount(txs);
});
updateNotes().then(() => {
// <HACK>
if (!lodash.isEmpty(foundLimitTx)) {
this.logger.debug('Tx history read until limitTx: ' + opts.limitTx);
return resolve(newHistory);
}
// </HACK>
var historyToSave = JSON.stringify(newHistory);
lodash.each(txs, (tx: any) => {
tx.recent = true;
});
this.logger.debug('Tx History synced. Total Txs: ' + newHistory.length);
// Final update
if (walletId == wallet.credentials.walletId) {
wallet.completeHistory = newHistory;
}
return this.persistenceProvider.setTxHistory(walletId, historyToSave).then(() => {
this.logger.debug('Tx History saved.');
return resolve();
}).catch((err) => {
return reject(err);
});
}).catch((err) => {
return reject(err);
});
}).catch((err) => {
/* TODO: update on progress
lodash.each(this.updateOnProgress[walletId], function(x) {
x.apply(this,err);
});
this.updateOnProgress[walletId] = false;
*/
return reject(err);
});
}).catch((err) => {
return reject(err);
});
});
}
private processNewTxs(wallet: any, txs: any): any[] {
let now = Math.floor(Date.now() / 1000);
let txHistoryUnique = {};
let ret = [];
wallet.hasUnsafeConfirmed = false;
lodash.each(txs, (tx: any) => {
tx = this.txFormatProvider.processTx(wallet.coin, tx);
// no future transactions...
if (tx.time > now)
tx.time = now;
if (tx.confirmations >= this.SAFE_CONFIRMATIONS) {
tx.safeConfirmed = this.SAFE_CONFIRMATIONS + '+';
} else {
tx.safeConfirmed = false;
wallet.hasUnsafeConfirmed = true;
};
if (tx.note) {
delete tx.note.encryptedEditedByName;
delete tx.note.encryptedBody;
};
if (!txHistoryUnique[tx.txid]) {
ret.push(tx);
txHistoryUnique[tx.txid] = true;
} else {
this.logger.debug('Ignoring duplicate TX in history: ' + tx.txid)
};
});
return ret;
}
public removeAndMarkSoftConfirmedTx(txs: any): any[] {
return lodash.filter(txs, (tx: any) => {
if (tx.confirmations >= this.SOFT_CONFIRMATION_LIMIT)
return tx;
tx.recent = true;
});
}
// Approx utxo amount, from which the uxto is economically redeemable
public getLowAmount(wallet: any): Promise<any> {
return new Promise((resolve, reject) => {
this.getMinFee(wallet).then((fee) => {
let minFee: number = fee;
return resolve(minFee / this.LOW_AMOUNT_RATIO);
}).catch((err) => {
return reject(err);
});
});
}
// Approx utxo amount, from which the uxto is economically redeemable
public getMinFee(wallet: any, nbOutputs?: number): Promise<any> {
return new Promise((resolve, reject) => {
this.feeProvider.getFeeLevels(wallet.coin).then((data: any) => {
let normalLevelRate: any = lodash.find(data.levels[wallet.network], (level: any) => {
return level.level === 'normal';
});
let lowLevelRate: string = (normalLevelRate.feePerKb / 1000).toFixed(0);
let size = this.getEstimatedTxSize(wallet, nbOutputs);
return resolve(size * parseInt(lowLevelRate, 10));
}).catch((err) => {
return reject(err);
});
})
};
// These 2 functions were taken from
// https://github.com/bitpay/bitcore-wallet-service/blob/master/lib/model/txproposal.js#L243
private getEstimatedSizeForSingleInput(wallet: any): number {
switch (wallet.credentials.addressType) {
case 'P2PKH':
return 147;
default:
case 'P2SH':
return wallet.m * 72 + wallet.n * 36 + 44;
};
}
private getEstimatedTxSize(wallet: any, nbOutputs?: number): number {
// Note: found empirically based on all multisig P2SH inputs and within m & n allowed limits.
nbOutputs = nbOutputs ? nbOutputs : 2; // Assume 2 outputs
let safetyMargin = 0.02;
let overhead = 4 + 4 + 9 + 9;
let inputSize = this.getEstimatedSizeForSingleInput(wallet);
let outputSize = 34;
let nbInputs = 1; // Assume 1 input
let size = overhead + inputSize * nbInputs + outputSize * nbOutputs;
return parseInt((size * (1 + safetyMargin)).toFixed(0), 10);
}
public getTxNote(wallet: any, txid: string): Promise<any> {
return new Promise((resolve, reject) => {
wallet.getTxNote({
txid
}, (err: any, note: any) => {
if (err) return reject(err);
return resolve(note);
});
});
}
public editTxNote(wallet: any, args: any): Promise<any> {
return new Promise((resolve, reject) => {
wallet.editTxNote(args, (err: any, res: any) => {
if (err) return reject(err);
return resolve(res);
});
});
}
public getTxp(wallet: any, txpid: string): Promise<any> {
return new Promise((resolve, reject) => {
wallet.getTx(txpid, (err: any, txp: any) => {
if (err) return reject(err);
return resolve(txp);
});
});
}
public getTx(wallet: any, txid: string) {
return new Promise((resolve, reject) => {
let finish = (list: any): any => {
let tx = lodash.find(list, {
txid
});
if (!tx) return reject('Could not get transaction');
return tx;
};
if (wallet.completeHistory && wallet.completeHistory.isValid) {
let tx = finish(wallet.completeHistory);
return resolve(tx);
} else {
let opts = {
force: true
};
this.getTxHistory(wallet, opts).then((txHistory: any) => {
let tx = finish(txHistory);
return resolve(tx);
}).catch((err) => {
return reject(err);
});
};
});
}
public getTxHistory(wallet: any, opts: any): Promise<any> {
return new Promise((resolve, reject) => {
opts = opts ? opts : {};
if (!wallet.isComplete()) return resolve();
let isHistoryCached = () => {
return wallet.completeHistory && wallet.completeHistory.isValid;
};
if (isHistoryCached() && !opts.force) return resolve(wallet.completeHistory);
this.logger.debug('Updating Transaction History');
this.updateLocalTxHistory(wallet, opts).then((txs: any) => {
if (opts.limitTx) {
return resolve(txs);
};
wallet.completeHistory.isValid = true;
return resolve(wallet.completeHistory);
}).catch((err) => {
return reject(err);
});
});
}
public isEncrypted(wallet: any): boolean {
if (lodash.isEmpty(wallet)) return;
let isEncrypted = wallet.isPrivKeyEncrypted();
if (isEncrypted) this.logger.debug('Wallet is encrypted');
return isEncrypted;
}
public createTx(wallet: any, txp: any) {
return new Promise((resolve, reject) => {
if (lodash.isEmpty(txp) || lodash.isEmpty(wallet))
return reject('MISSING_PARAMETER');
wallet.createTxProposal(txp, (err: any, createdTxp: any) => {
if (err) return reject(err);
else {
this.logger.debug('Transaction created');
return resolve(createdTxp);
};
});
});
}
public publishTx(wallet: any, txp: any): Promise<any> {
return new Promise((resolve, reject) => {
if (lodash.isEmpty(txp) || lodash.isEmpty(wallet))
return reject('MISSING_PARAMETER');
wallet.publishTxProposal({
txp
}, (err: any, publishedTx: any) => {
if (err) return reject(err);
else {
this.logger.debug('Transaction published');
return resolve(publishedTx);
};
});
});
}
signTx(wallet: any, txp: any, password: string): Promise<any> {
return new Promise((resolve, reject) => {
if (!wallet || !txp)
return reject('MISSING_PARAMETER');
try {
wallet.signTxProposal(txp, password, (err: any, signedTxp: any) => {
this.logger.debug('Transaction signed err:' + err);
if (err) return reject(err);
return resolve(signedTxp);
});
} catch (e) {
this.logger.warn('Error at signTxProposal:', e);
return reject(e);
};
});
}
public broadcastTx(wallet: any, txp: any): Promise<any> {
return new Promise((resolve, reject) => {
if (lodash.isEmpty(txp) || lodash.isEmpty(wallet))
return reject('MISSING_PARAMETER');
if (txp.status != 'accepted')
return reject('TX_NOT_ACCEPTED');
wallet.broadcastTxProposal(txp, (err: any, broadcastedTxp: any, memo: any) => {
if (err)
return reject(err);
this.logger.debug('Transaction broadcasted');
if (memo) this.logger.info(memo);
return resolve(broadcastedTxp);
});
});
}
public rejectTx(wallet: any, txp: any): Promise<any> {
return new Promise((resolve, reject) => {
if (lodash.isEmpty(txp) || lodash.isEmpty(wallet))
return reject('MISSING_PARAMETER');
wallet.rejectTxProposal(txp, null, (err: any, rejectedTxp: any) => {
if (err)
return reject(err);
this.logger.debug('Transaction rejected');
return resolve(rejectedTxp);
});
});
}
public removeTx(wallet: any, txp: any): Promise<any> {
return new Promise((resolve, reject) => {
if (lodash.isEmpty(txp) || lodash.isEmpty(wallet))
return reject('MISSING_PARAMETER');
wallet.removeTxProposal(txp, (err: any) => {
this.logger.debug('Transaction removed');
this.invalidateCache(wallet);
this.events.publish('Local/TxAction', wallet.id);
return resolve(err);
});
});
}
public updateRemotePreferences(clients: any, prefs?: any): Promise<any> {
return new Promise((resolve, reject) => {
prefs = prefs ? prefs : {};
if (!lodash.isArray(clients))
clients = [clients];
let updateRemotePreferencesFor = (clients: any, prefs: any): Promise<any> => {
return new Promise((resolve, reject) => {
let wallet = clients.shift();
if (!wallet) return resolve();
this.logger.debug('Saving remote preferences', wallet.credentials.walletName, prefs);
wallet.savePreferences(prefs, (err: any) => {
if (err) {
this.popupProvider.ionicAlert(this.bwcErrorProvider.msg(err, this.translate.instant('Could not save preferences on the server')));
return reject(err);
}
updateRemotePreferencesFor(clients, prefs).then(() => {
return resolve();
}).catch((err: any) => {
return reject(err);
});
});
});
};
// Update this JIC.
let config: any = this.configProvider.get();
// TODO prefs.email (may come from arguments)
prefs.email = config.emailNotifications.email;
prefs.language = "en" // This line was hardcoded - TODO: prefs.language = uxLanguage.getCurrentLanguage();
// TODO let walletSettings = config.wallet.settings;
// prefs.unit = walletSettings.unitCode; // TODO: remove, not used
updateRemotePreferencesFor(lodash.clone(clients), prefs).then(() => {
this.logger.debug('Remote preferences saved for' + lodash.map(clients, (x: any) => {
return x.credentials.walletId;
}).join(','));
lodash.each(clients, (c: any) => {
c.preferences = lodash.assign(prefs, c.preferences);
});
return resolve();
}).catch((err: any) => {
return reject(err);
});
});
}
public recreate(wallet: any): Promise<any> {
return new Promise((resolve, reject) => {
this.logger.debug('Recreating wallet:', wallet.id);
wallet.recreateWallet((err: any) => {
wallet.notAuthorized = false;
if (err) return reject(err);
return resolve();
});
});
}
public startScan(wallet: any): Promise<any> {
return new Promise((resolve, reject) => {
this.logger.debug('Scanning wallet ' + wallet.id);
if (!wallet.isComplete()) return reject();
wallet.scanning = true;
wallet.startScan({
includeCopayerBranches: true,
}, (err: any) => {
if (err) return reject(err);
return resolve();
});
});
}
public clearTxHistory(wallet): void {
this.invalidateCache(wallet);
this.persistenceProvider.removeTxHistory(wallet.id);
}
public expireAddress(wallet: any): Promise<any> {
return new Promise((resolve, reject) => {
this.logger.debug('Cleaning Address ' + wallet.id);
this.persistenceProvider.clearLastAddress(wallet.id).then(() => {
return resolve();
}).catch((err: any) => {
return reject(err);
});
});
}
public getMainAddresses(wallet: any, opts: any): Promise<any> {
return new Promise((resolve, reject) => {
opts = opts || {};
opts.reverse = true;
wallet.getMainAddresses(opts, (err, addresses) => {
if (err) return reject(err);
return resolve(addresses);
});
});
}
public getBalance(wallet: any, opts: any): Promise<any> {
return new Promise((resolve, reject) => {
opts = opts || {};
wallet.getBalance(opts, (err, resp) => {
if (err) return reject(err);
return resolve(resp);
});
});
}
public getLowUtxos(wallet: any): Promise<any> {
return new Promise((resolve, reject) => {
wallet.getUtxos({
coin: wallet.coin
}, (err, resp) => {
if (err || !resp || !resp.length) return reject(err);
this.getMinFee(wallet, resp.length).then((fee) => {
let minFee = fee;
let balance = lodash.sumBy(resp, 'satoshis');
// for 2 outputs
this.getLowAmount(wallet).then((fee) => {
let lowAmount = fee;
let lowUtxos = lodash.filter(resp, (x: any) => {
return x.satoshis < lowAmount;
});
let totalLow = lodash.sumBy(lowUtxos, 'satoshis');
return resolve({
allUtxos: resp || [],
lowUtxos: lowUtxos || [],
totalLow,
warning: minFee / balance > this.TOTAL_LOW_WARNING_RATIO,
minFee,
});
});
});
});
});
}
// An alert dialog
private askPassword(name: string, title: string): Promise<any> {
return new Promise((resolve, reject) => {
let opts = {
type: 'password'
}
this.popupProvider.ionicPrompt(title, name, opts).then((res: any) => {
return resolve(res);
});
});
}
public encrypt(wallet: any): Promise<any> {
return new Promise((resolve, reject) => {
var title = this.translate.instant('Enter new spending password');
var warnMsg = this.translate.instant('Your wallet key will be encrypted. The Spending Password cannot be recovered. Be sure to write it down.');
this.askPassword(warnMsg, title).then((password: string) => {
if (!password) return reject(this.translate.instant('no password'));
title = this.translate.instant('Confirm your new spending password');
this.askPassword(warnMsg, title).then((password2: string) => {
if (!password2 || password != password2) return reject(this.translate.instant('password mismatch'));
wallet.encryptPrivateKey(password);
return resolve();
}).catch((err) => {
return reject(err);
});
}).catch((err) => {
return reject(err);
});
});
}
public decrypt(wallet: any): Promise<any> {
return new Promise((resolve, reject) => {
this.logger.debug('Disabling private key encryption for' + wallet.name);
this.askPassword(null, this.translate.instant('Enter Spending Password')).then((password: string) => {
if (!password) return reject(this.translate.instant('no password'));
try {
wallet.decryptPrivateKey(password);
} catch (e) {
return reject(e);
}
return resolve();
});
});
}
public handleEncryptedWallet(wallet: any): Promise<any> {
return new Promise((resolve, reject) => {
if (!this.isEncrypted(wallet)) return resolve();
this.askPassword(wallet.name, this.translate.instant('Enter Spending Password')).then((password: string) => {
if (!password) return reject(this.translate.instant('No password'));
if (!wallet.checkPassword(password)) return reject(this.translate.instant('Wrong password'));
return resolve(password);
});
});
}
public reject(wallet: any, txp: any): Promise<any> {
return new Promise((resolve, reject) => {
this.rejectTx(wallet, txp).then((txpr: any) => {
this.invalidateCache(wallet);
this.events.publish('Local/TxAction', wallet.id);
return resolve(txpr);
}).catch((err) => {
return reject(err);
});
});
}
public onlyPublish(wallet: any, txp: any): Promise<any> {
return new Promise((resolve, reject) => {
this.publishTx(wallet, txp).then((publishedTxp) => {
this.invalidateCache(wallet);
this.events.publish('Local/TxAction', wallet.id);
return resolve();
}).catch((err) => {
return reject(this.bwcErrorProvider.msg(err));
});
});
}
public prepare(wallet: any): Promise<any> {
return new Promise((resolve, reject) => {
this.touchidProvider.checkWallet(wallet).then(() => {
this.handleEncryptedWallet(wallet).then((password: string) => {
return resolve(password);
}).catch((err) => {
return reject(err);
});
}).catch((err) => {
return reject(err);
});
});
}
private signAndBroadcast(wallet: any, publishedTxp: any, password: any): Promise<any> {
return new Promise((resolve, reject) => {
this.onGoingProcessProvider.set('signingTx');
this.signTx(wallet, publishedTxp, password).then((signedTxp: any) => {
this.invalidateCache(wallet);
if (signedTxp.status == 'accepted') {
this.onGoingProcessProvider.set('broadcastingTx');
this.broadcastTx(wallet, signedTxp).then((broadcastedTxp: any) => {
this.events.publish('Local/TxAction', wallet.id);
return resolve(broadcastedTxp);
}).catch((err) => {
return reject(this.bwcErrorProvider.msg(err));
});
} else {
this.events.publish('Local/TxAction', wallet.id);
return resolve(signedTxp);
};
}).catch((err) => {
this.logger.warn('sign error:' + err);
let msg = err && err.message ? err.message : this.translate.instant('The payment was created but could not be completed. Please try again from home screen');
this.events.publish('Local/TxAction', wallet.id);
return reject(msg);
});
});
}
public publishAndSign(wallet: any, txp: any): Promise<any> {
return new Promise((resolve, reject) => {
// Already published?
if (txp.status == 'pending') {
this.prepare(wallet).then((password: string) => {
this.signAndBroadcast(wallet, txp, password).then((broadcastedTxp: any) => {
return resolve(broadcastedTxp);
}).catch((err) => {
return reject(err);
});
}).catch((err) => {
return reject(this.bwcErrorProvider.msg(err));
});
} else {
this.prepare(wallet).then((password: string) => {
this.onGoingProcessProvider.set('sendingTx');
this.publishTx(wallet, txp).then((publishedTxp: any) => {
this.signAndBroadcast(wallet, publishedTxp, password).then((broadcastedTxp: any) => {
return resolve(broadcastedTxp);
}).catch((err) => {
return reject(err);
});
}).catch((err) => {
return reject(this.bwcErrorProvider.msg(err));
});
}).catch((err) => {
return reject(this.bwcErrorProvider.msg(err));
});
};
});
}
public getEncodedWalletInfo(wallet: any, password?: string): Promise<any> {
return new Promise((resolve, reject) => {
let derivationPath = wallet.credentials.getBaseAddressDerivationPath();
let encodingType = {
mnemonic: 1,
xpriv: 2,
xpub: 3
};
let info: any = {};
// not supported yet
if (wallet.credentials.derivationStrategy != 'BIP44' || !wallet.canSign())
return reject(this.translate.instant('Exporting via QR not supported for this wallet'));
var keys = this.getKeysWithPassword(wallet, password);
if (keys.mnemonic) {
info = {
type: encodingType.mnemonic,
data: keys.mnemonic,
}
} else {
info = {
type: encodingType.xpriv,
data: keys.xPrivKey
}
}
return resolve(info.type + '|' + info.data + '|' + wallet.credentials.network.toLowerCase() + '|' + derivationPath + '|' + (wallet.credentials.mnemonicHasPassphrase));
});
}
public getKeysWithPassword(wallet: any, password: string): any {
try {
return wallet.getKeys(password);
} catch (e) {
this.logger.debug(e);
}
}
public setTouchId(wallet: any, enabled: boolean): Promise<any> {
return new Promise((resolve, reject) => {
let opts = {
touchIdFor: {}
};
opts.touchIdFor[wallet.id] = enabled;
this.touchidProvider.checkWallet(wallet).then(() => {
this.configProvider.set(opts);
return resolve();
}).catch((err) => {
opts.touchIdFor[wallet.id] = !enabled;
this.logger.debug('Error with fingerprint:' + err);
this.configProvider.set(opts);
return reject(err);
});
});
}
public getKeys(wallet: any): Promise<any> {
return new Promise((resolve, reject) => {
this.prepare(wallet).then((password: string) => {
let keys;
try {
keys = wallet.getKeys(password);
} catch (e) {
return reject(e);
}
return resolve(keys);
}).catch((err) => {
return reject(err);
});
});
}
public getSendMaxInfo(wallet: any, opts: any): Promise<any> {
return new Promise((resolve, reject) => {
opts = opts || {};
wallet.getSendMaxInfo(opts, (err: any, res: any) => {
if (err) return reject(err);
return resolve(res);
});
});
}
public getProtocolHandler(coin: string): string {
if (coin == 'bch') {
return 'bitcoincash';
} else {
return 'bitcoin';
}
}
public copyCopayers(wallet: any, newWallet: any): Promise<any> {
return new Promise((resolve, reject) => {
let walletPrivKey = this.bwcProvider.getBitcore().PrivateKey.fromString(wallet.credentials.walletPrivKey);
let copayer = 1;
let i = 0;
lodash.each(wallet.credentials.publicKeyRing, (item) => {
let name = item.copayerName || ('copayer ' + copayer++);
newWallet._doJoinWallet(newWallet.credentials.walletId, walletPrivKey, item.xPubKey, item.requestPubKey, name, {
coin: newWallet.credentials.coin,
}, (err: any) => {
// Ignore error is copayer already in wallet
if (err && !(err instanceof this.errors.COPAYER_IN_WALLET)) return reject(err);
if (++i == wallet.credentials.publicKeyRing.length) return resolve();
});
});
});
};
}