UNPKG

copay-crown

Version:

A Secure Crown Wallet

1,345 lines (1,145 loc) 44.6 kB
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'; @Injectable() 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(); }); }); }); }; }