copay-crown
Version:
A Secure Crown Wallet
1,210 lines (1,025 loc) • 38.4 kB
text/typescript
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Events } from 'ionic-angular';
import * as _ from 'lodash';
import { Logger } from '../../providers/logger/logger';
// providers
import { AppProvider } from '../../providers/app/app';
import { LanguageProvider } from '../../providers/language/language';
import { BwcErrorProvider } from '../bwc-error/bwc-error';
import { BwcProvider } from '../bwc/bwc';
import { ConfigProvider } from '../config/config';
import { OnGoingProcessProvider } from '../on-going-process/on-going-process';
import { PersistenceProvider } from '../persistence/persistence';
import { PlatformProvider } from '../platform/platform';
import { PopupProvider } from '../popup/popup';
// models
import { Profile } from '../../models/profile/profile.model';
export class ProfileProvider {
public wallet: any = {};
public profile: Profile;
private UPDATE_PERIOD = 15;
private throttledBwsEvent: any;
private validationLock: boolean = false;
private errors: any = this.bwcProvider.getErrors();
constructor(
private logger: Logger,
private persistenceProvider: PersistenceProvider,
private configProvider: ConfigProvider,
private bwcProvider: BwcProvider,
private bwcErrorProvider: BwcErrorProvider,
private platformProvider: PlatformProvider,
private appProvider: AppProvider,
private languageProvider: LanguageProvider,
private events: Events,
private popupProvider: PopupProvider,
private onGoingProcessProvider: OnGoingProcessProvider,
private translate: TranslateService
) {
this.throttledBwsEvent = _.throttle((n, wallet) => {
this.newBwsEvent(n, wallet);
}, 10000);
}
private updateWalletSettings(wallet: any): void {
let config: any = this.configProvider.get();
let defaults: any = this.configProvider.getDefaults();
// this.config.whenAvailable( (config) => { TODO
wallet.usingCustomBWS = config.bwsFor && config.bwsFor[wallet.id] && (config.bwsFor[wallet.id] != defaults.bws.url);
wallet.name = (config.aliasFor && config.aliasFor[wallet.id]) || wallet.credentials.walletName;
wallet.color = (config.colorFor && config.colorFor[wallet.id]) ? config.colorFor[wallet.id] : null;
wallet.email = config.emailFor && config.emailFor[wallet.id];
// });
}
public setWalletOrder(walletId: string, index: number): void {
this.persistenceProvider.setWalletOrder(walletId, index);
this.logger.debug('Wallet new order stored');
this.wallet[walletId].order = index;
}
private getWalletOrder(wallet): Promise<any> {
return new Promise((resolve, reject) => {
this.persistenceProvider.getWalletOrder(wallet.credentials.walletId).then((order) => {
return resolve(order);
});
})
}
public setBackupFlag(walletId: string): void {
this.persistenceProvider.setBackupFlag(walletId);
this.logger.debug('Backup flag stored');
this.wallet[walletId].needsBackup = false;
}
private requiresBackup(wallet: any): boolean {
if (wallet.isPrivKeyExternal()) return false;
if (!wallet.credentials.mnemonic && !wallet.credentials.mnemonicEncrypted) return false;
if (wallet.credentials.network == 'testnet') return false;
return true;
}
private needsBackup(wallet: any): Promise<boolean> {
return new Promise((resolve, reject) => {
if (!this.requiresBackup(wallet)) {
return resolve(false);
}
this.persistenceProvider.getBackupFlag(wallet.credentials.walletId).then((val: string) => {
if (val) {
return resolve(false);
}
return resolve(true);
}).catch((err) => {
this.logger.error(err);
});
})
}
private isBalanceHidden(wallet: any): Promise<boolean> {
return new Promise((resolve, reject) => {
this.persistenceProvider.getHideBalanceFlag(wallet.credentials.walletId).then((shouldHideBalance) => {
let isHidden = shouldHideBalance && shouldHideBalance.toString() == 'true' ? true : false;
return resolve(isHidden);
}).catch((err) => {
this.logger.error(err);
});
});
}
private bindWalletClient(wallet: any, opts?: any): boolean {
opts = opts ? opts : {};
let walletId = wallet.credentials.walletId;
if ((this.wallet[walletId] && this.wallet[walletId].started) && !opts.force) return false;
// INIT WALLET VIEWMODEL
wallet.id = walletId;
wallet.started = true;
wallet.network = wallet.credentials.network;
wallet.copayerId = wallet.credentials.copayerId;
wallet.m = wallet.credentials.m;
wallet.n = wallet.credentials.n;
wallet.coin = wallet.credentials.coin;
wallet.status = {};
this.updateWalletSettings(wallet);
this.wallet[walletId] = wallet;
this.needsBackup(wallet).then((val: any) => {
wallet.needsBackup = val;
});
this.isBalanceHidden(wallet).then((val: any) => {
wallet.balanceHidden = val;
});
this.getWalletOrder(wallet).then((val: number) => {
wallet.order = val;
});
wallet.removeAllListeners();
wallet.on('report', (n: any) => {
this.logger.info('BWC Report:' + n);
});
wallet.on('notification', (n: any) => {
this.logger.debug('BWC Notification:', JSON.stringify(n));
if (n.type == "NewBlock" && n.data.network == "testnet") {
this.throttledBwsEvent(n, wallet);
} else this.newBwsEvent(n, wallet);
});
wallet.on('walletCompleted', () => {
this.logger.debug('Wallet completed');
this.updateCredentials(JSON.parse(wallet.export()))
this.events.publish('status:updated');
});
wallet.initialize({
notificationIncludeOwn: true,
}, (err: any) => {
if (err) {
this.logger.error('Could not init notifications err:', err);
return;
}
wallet.setNotificationsInterval(this.UPDATE_PERIOD);
wallet.openWallet((err: any) => {
if (wallet.status !== true)
this.logger.debug('Wallet + ' + walletId + ' status:' + JSON.stringify(wallet.status));
});
});
this.events.subscribe('wallet:updated', (walletId: string) => {
if (walletId && walletId == wallet.id) {
this.logger.debug('Updating settings for wallet:' + wallet.id);
this.updateWalletSettings(wallet);
}
});
return true;
}
private newBwsEvent(n: any, wallet: any): void {
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;
this.events.publish('bwsEvent', wallet.id, n.type, n);
}
public updateCredentials(credentials: any): void {
this.profile.updateWallet(credentials);
this.persistenceProvider.storeProfile(this.profile);
}
public getLastKnownBalance(wid: string): Promise<string> {
return new Promise((resolve, reject) => {
this.persistenceProvider.getBalanceCache(wid).then((data: string) => {
return resolve(data);
}).catch((err: any) => {
return reject(err);
});
});
}
private addLastKnownBalance(wallet: any): Promise<any> {
return new Promise((resolve, reject) => {
let now = Math.floor(Date.now() / 1000);
let showRange = 600; // 10min;
this.getLastKnownBalance(wallet.id).then((data: any) => {
if (data) {
let parseData: any = data;
wallet.cachedBalance = parseData.balance;
wallet.cachedBalanceUpdatedOn = (parseData.updatedOn < now - showRange) ? parseData.updatedOn : null;
}
return resolve();
}).catch((err: any) => {
this.logger.warn(err);
});
});
}
public setLastKnownBalance(wid: string, balance: number): void {
this.persistenceProvider.setBalanceCache(wid, { balance, updatedOn: Math.floor(Date.now() / 1000) });
}
private runValidation(wallet: any, delay?: number, retryDelay?: number) {
delay = delay ? delay : 500;
retryDelay = retryDelay ? retryDelay : 50;
if (this.validationLock) {
return setTimeout(() => {
this.logger.debug('ValidatingWallet Locked: Retrying in: ' + retryDelay);
return this.runValidation(wallet, delay, retryDelay);
}, retryDelay);
}
this.validationLock = true;
// IOS devices are already checked
let skipDeviceValidation = this.platformProvider.isIOS || this.profile.isDeviceChecked(this.platformProvider.ua);
let walletId = wallet.credentials.walletId;
this.logger.debug('ValidatingWallet: ' + walletId + ' skip Device:' + skipDeviceValidation);
setTimeout(() => {
wallet.validateKeyDerivation({
skipDeviceValidation,
}, (err: any, isOK: any) => {
this.validationLock = false;
this.logger.debug('ValidatingWallet End: ' + walletId + ' isOK:' + isOK);
if (isOK) {
this.profile.setChecked(this.platformProvider.ua, walletId);
} else {
this.logger.warn('Key Derivation failed for wallet:' + walletId);
this.persistenceProvider.clearLastAddress(walletId).then(() => {
});
};
this.storeProfileIfDirty();
});
}, delay);
}
public storeProfileIfDirty(): void {
if (this.profile.dirty) {
this.persistenceProvider.storeProfile(this.profile).then((err: any) => {
this.logger.debug('Saved modified Profile');
return;
});
} else {
return;
};
}
public importWallet(str: string, opts: any): Promise<any> {
return new Promise((resolve, reject) => {
let walletClient = this.bwcProvider.getClient(null, opts);
this.logger.debug('Importing Wallet:', opts);
try {
let c = JSON.parse(str);
if (c.xPrivKey && c.xPrivKeyEncrypted) {
this.logger.warn('Found both encrypted and decrypted key. Deleting the encrypted version');
delete c.xPrivKeyEncrypted;
delete c.mnemonicEncrypted;
}
str = JSON.stringify(c);
walletClient.import(str, {
compressed: opts.compressed,
password: opts.password
});
} catch (err) {
return reject(this.translate.instant('Could not import. Check input file and spending password'));
}
let strParsed: any = JSON.parse(str);
if (!strParsed.n) {
return reject("Backup format not recognized. If you are using a Copay Beta backup and version is older than 0.10, please see: https://github.com/bitpay/copay/issues/4730#issuecomment-244522614");
}
let addressBook = strParsed.addressBook ? strParsed.addressBook : {};
this.addAndBindWalletClient(walletClient, {
bwsurl: opts.bwsurl
}).then((walletId: string) => {
this.setMetaData(walletClient, addressBook).then(() => {
return resolve(walletClient);
}).catch((err: any) => {
this.logger.warn(err);
return reject(err);
});
}).catch((err: any) => {
return reject(err);
});
});
}
// 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);
});
});
}
private showWarningNoEncrypt(): Promise<any> {
return new Promise((resolve, reject) => {
let title = this.translate.instant('Are you sure?');
let msg = this.translate.instant('Without encryption, a thief or another application on this device may be able to access your funds.');
let okText = this.translate.instant('I\'m sure');
let cancelText = this.translate.instant('Go Back');
this.popupProvider.ionicConfirm(title, msg, okText, cancelText).then((res: any) => {
return resolve(res);
});
});
}
private askToEncryptWallet(wallet: any): Promise<any> {
return new Promise((resolve, reject) => {
var title = this.translate.instant('Would you like to protect this wallet with a password?');
var message = this.translate.instant('Encryption can protect your funds if this device is stolen or compromised by malicious software.');
var okText = this.translate.instant('Yes');
var cancelText = this.translate.instant('No');
this.popupProvider.ionicConfirm(title, message, okText, cancelText).then((res: any) => {
if (!res) {
return this.showWarningNoEncrypt().then((res) => {
if (res) return resolve()
return this.encrypt(wallet).then(() => {
return resolve();
});
});
}
return this.encrypt(wallet).then(() => {
return resolve();
});
});
});
}
private encrypt(wallet: any): Promise<any> {
return new Promise((resolve, reject) => {
let title = this.translate.instant('Enter a password to encrypt your wallet');
let warnMsg = this.translate.instant('This password is only for this device, and it cannot be recovered. To avoid losing funds, write your password down.');
this.askPassword(warnMsg, title).then((password: string) => {
if (!password) {
this.showWarningNoEncrypt().then((res: any) => {
if (res) return resolve();
this.encrypt(wallet).then(() => {
return resolve();
});
});
}
else {
title = this.translate.instant('Enter your password again to confirm');
this.askPassword(warnMsg, title).then((password2: string) => {
if (!password2 || password != password2) {
this.encrypt(wallet).then(() => {
return resolve();
});
} else {
wallet.encryptPrivateKey(password);
return resolve();
}
});
}
});
});
}
// Adds and bind a new client to the profile
private addAndBindWalletClient(wallet: any, opts: any): Promise<any> {
return new Promise((resolve, reject) => {
if (!wallet || !wallet.credentials) {
return reject(this.translate.instant('Could not access wallet'));
}
// Encrypt wallet
this.onGoingProcessProvider.pause();
this.askToEncryptWallet(wallet).then(() => {
this.onGoingProcessProvider.resume();
let walletId: string = wallet.credentials.walletId
if (!this.profile.addWallet(JSON.parse(wallet.export()))) {
let message = "Wallet already in " + this.appProvider.info.nameCase; // TODO: translate
return reject(message);
}
let skipKeyValidation: boolean = this.shouldSkipValidation(walletId);
if (!skipKeyValidation)
this.runValidation(wallet);
this.bindWalletClient(wallet);
let saveBwsUrl = (): Promise<any> => {
return new Promise((resolve, reject) => {
let defaults: any = this.configProvider.getDefaults();
let bwsFor: any = {};
bwsFor[walletId] = opts.bwsurl || defaults.bws.url;
// Dont save the default
if (bwsFor[walletId] == defaults.bws.url) {
return resolve();
}
this.configProvider.set({ bwsFor });
return resolve();
});
};
saveBwsUrl().then(() => {
this.persistenceProvider.storeProfile(this.profile).then(() => {
return resolve(wallet);
}).catch((err: any) => {
return reject(err);
});
});
});
});
}
private shouldSkipValidation(walletId: string): boolean {
return this.profile.isChecked(this.platformProvider.ua, walletId) || this.platformProvider.isIOS;
}
private setMetaData(wallet: any, addressBook: any): Promise<any> {
return new Promise((resolve, reject) => {
this.persistenceProvider.getAddressbook(wallet.credentials.network).then((localAddressBook: any) => {
let localAddressBook1 = {};
try {
localAddressBook1 = JSON.parse(localAddressBook);
} catch (ex) {
this.logger.warn(ex);
}
let mergeAddressBook = _.merge(addressBook, localAddressBook1);
this.persistenceProvider.setAddressbook(wallet.credentials.network, JSON.stringify(mergeAddressBook)).then(() => {
return resolve();
}).catch((err: any) => {
return reject(err);
});
}).catch((err: any) => {
return reject(err);
});
});
}
public importExtendedPrivateKey(xPrivKey: string, opts: any): Promise<any> {
return new Promise((resolve, reject) => {
let walletClient = this.bwcProvider.getClient(null, opts);
this.logger.debug('Importing Wallet xPrivKey');
walletClient.importFromExtendedPrivateKey(xPrivKey, opts, (err: any) => {
if (err) {
if (err instanceof this.errors.NOT_AUTHORIZED) return reject(err);
this.bwcErrorProvider.cb(err, this.translate.instant('Could not import')).then((msg: string) => {
return reject(msg);
});
} else {
this.addAndBindWalletClient(walletClient, {
bwsurl: opts.bwsurl
}).then((wallet: any) => {
return resolve(wallet);
}).catch((err: any) => {
return reject(err);
});
};
});
});
}
private normalizeMnemonic(words: string): string {
if (!words || !words.indexOf) return words;
let isJA = words.indexOf('\u3000') > -1;
let wordList = words.split(/[\u3000\s]+/);
return wordList.join(isJA ? '\u3000' : ' ');
};
public importMnemonic(words: string, opts: any): Promise<any> {
return new Promise((resolve, reject) => {
let walletClient = this.bwcProvider.getClient(null, opts);
this.logger.debug('Importing Wallet Mnemonic');
words = this.normalizeMnemonic(words);
walletClient.importFromMnemonic(words, {
network: opts.networkName,
passphrase: opts.passphrase,
entropySourcePath: opts.entropySourcePath,
derivationStrategy: opts.derivationStrategy || 'BIP44',
account: opts.account || 0,
coin: opts.coin
}, (err: any) => {
if (err) {
if (err instanceof this.errors.NOT_AUTHORIZED) {
return reject(err);
}
this.bwcErrorProvider.cb(err, this.translate.instant('Could not import')).then((msg: string) => {
return reject(msg);
});
}
this.addAndBindWalletClient(walletClient, {
bwsurl: opts.bwsurl
}).then((wallet: any) => {
return resolve(wallet);
}).catch((err: any) => {
return reject(err);
});
});
});
}
public importExtendedPublicKey(opts: any): Promise<any> {
return new Promise((resolve, reject) => {
let walletClient = this.bwcProvider.getClient(null, opts);
this.logger.debug('Importing Wallet XPubKey');
walletClient.importFromExtendedPublicKey(opts.extendedPublicKey, opts.externalSource, opts.entropySource, {
account: opts.account || 0,
derivationStrategy: opts.derivationStrategy || 'BIP44',
coin: opts.coin
}, (err: any) => {
if (err) {
// in HW wallets, req key is always the same. They can't addAccess.
if (err instanceof this.errors.NOT_AUTHORIZED)
err.name = 'WALLET_DOES_NOT_EXIST';
this.bwcErrorProvider.cb(err, this.translate.instant('Could not import')).then((msg: string) => {
return reject(msg);
});
}
this.addAndBindWalletClient(walletClient, {
bwsurl: opts.bwsurl
}).then((wallet: any) => {
return resolve(wallet);
}).catch((err: any) => {
return reject(err);
});
});
});
}
public createProfile(): void {
this.logger.info('Creating profile');
this.profile = new Profile();
this.profile = this.profile.create();
this.persistenceProvider.storeNewProfile(this.profile);
}
public bindProfile(profile: any): Promise<any> {
return new Promise((resolve, reject) => {
let bindWallets = (): Promise<any> => {
return new Promise((resolve, reject) => {
let l = profile.credentials.length;
let i = 0;
let totalBound = 0;
if (!l) {
return resolve();
}
_.each(profile.credentials, (credentials) => {
this.bindWallet(credentials).then((bound: number) => {
i++;
totalBound += bound;
if (i == l) {
this.logger.info('Bound ' + totalBound + ' out of ' + l + ' wallets');
return resolve();
}
}).catch((err) => {
return reject(err);
});
});
});
};
bindWallets().then(() => {
this.isOnboardingCompleted().then(() => {
this.isDisclaimerAccepted().then(() => {
return resolve();
}).catch(() => {
return reject(new Error('NONAGREEDDISCLAIMER: Non agreed disclaimer'));
});
}).catch(() => {
this.isDisclaimerAccepted().then(() => {
this.setOnboardingCompleted().then(() => {
return resolve();
}).catch((err: any) => {
this.logger.error(err);
});
}).catch(() => {
return reject(new Error('ONBOARDINGNONCOMPLETED: Onboarding non completed'));
});
});
}).catch((err: any) => {
return reject(err);
});
});
}
public isDisclaimerAccepted(): Promise<any> {
return new Promise((resolve, reject) => {
let disclaimerAccepted = this.profile && this.profile.disclaimerAccepted;
if (disclaimerAccepted) return resolve();
// OLD flag
this.persistenceProvider.getCopayDisclaimerFlag().then((val) => {
if (val) {
this.profile.disclaimerAccepted = true;
return resolve();
} else {
return reject();
}
});
});
}
public isOnboardingCompleted(): Promise<any> {
return new Promise((resolve, reject) => {
let onboardingCompleted = this.profile && this.profile.onboardingCompleted;
if (onboardingCompleted) return resolve();
this.persistenceProvider.getCopayOnboardingFlag().then((val) => {
if (val) {
this.profile.onboardingCompleted = true;
return resolve();
} else {
return reject();
}
});
});
}
private bindWallet(credentials: any): Promise<any> {
return new Promise((resolve, reject) => {
if (!credentials.walletId || !credentials.m) {
return reject('bindWallet should receive credentials JSON');
}
// Create the client
let getBWSURL = (walletId: string) => {
let config: any = this.configProvider.get();
let defaults: any = this.configProvider.getDefaults();
return ((config.bwsFor && config.bwsFor[walletId]) || defaults.bws.url);
};
let walletClient = this.bwcProvider.getClient(JSON.stringify(credentials), {
bwsurl: getBWSURL(credentials.walletId),
});
let skipKeyValidation = this.shouldSkipValidation(credentials.walletId);
if (!skipKeyValidation) this.runValidation(walletClient, 500);
this.logger.info('Binding wallet:' + credentials.walletId + ' Validating?:' + !skipKeyValidation);
return resolve(this.bindWalletClient(walletClient));
});
}
public loadAndBindProfile(): Promise<any> {
return new Promise((resolve, reject) => {
this.persistenceProvider.getProfile().then((profile: any) => {
if (!profile) {
return resolve();
}
this.profile = new Profile();
this.profile = this.profile.fromObj(profile);
// Deprecated: storageService.tryToMigrate
this.logger.debug('Profile read');
this.bindProfile(this.profile).then(() => {
return resolve(this.profile);
}).catch((err: any) => {
return reject(err);
});
}).catch((err: any) => {
return reject(err);
});
});
}
private seedWallet(opts: any): Promise<any> {
return new Promise((resolve, reject) => {
opts = opts ? opts : {};
let walletClient = this.bwcProvider.getClient(null, opts);
let network = opts.networkName || 'livenet';
if (opts.mnemonic) {
try {
opts.mnemonic = this.normalizeMnemonic(opts.mnemonic);
walletClient.seedFromMnemonic(opts.mnemonic, {
network,
passphrase: opts.passphrase,
account: opts.account || 0,
derivationStrategy: opts.derivationStrategy || 'BIP44',
coin: opts.coin
});
} catch (ex) {
this.logger.info(ex);
return reject(this.translate.instant('Could not create: Invalid wallet recovery phrase'));
}
} else if (opts.extendedPrivateKey) {
try {
walletClient.seedFromExtendedPrivateKey(opts.extendedPrivateKey, {
network,
account: opts.account || 0,
derivationStrategy: opts.derivationStrategy || 'BIP44',
coin: opts.coin,
});
} catch (ex) {
this.logger.warn(ex);
return reject(this.translate.instant('Could not create using the specified extended private key'));
}
} else if (opts.extendedPublicKey) {
try {
walletClient.seedFromExtendedPublicKey(opts.extendedPublicKey, opts.externalSource, opts.entropySource, {
account: opts.account || 0,
derivationStrategy: opts.derivationStrategy || 'BIP44',
coin: opts.coin
});
walletClient.credentials.hwInfo = opts.hwInfo;
} catch (ex) {
this.logger.warn("Creating wallet from Extended Public Key Arg:", ex, opts);
return reject(this.translate.instant('Could not create using the specified extended public key'));
}
} else {
let lang = this.languageProvider.getCurrent();
try {
walletClient.seedFromRandomWithMnemonic({
network,
passphrase: opts.passphrase,
language: lang,
account: 0,
coin: opts.coin
});
} catch (e) {
this.logger.info('Error creating recovery phrase: ' + e.message);
if (e.message.indexOf('language') > 0) {
this.logger.info('Using default language for recovery phrase');
walletClient.seedFromRandomWithMnemonic({
network,
passphrase: opts.passphrase,
account: 0,
coin: opts.coin
});
} else {
return reject(e);
}
}
}
return resolve(walletClient);
});
}
// Creates a wallet on BWC/BWS
private doCreateWallet(opts: any): Promise<any> {
return new Promise((resolve, reject) => {
let showOpts = _.clone(opts);
if (showOpts.extendedPrivateKey) showOpts.extendedPrivateKey = '[hidden]';
if (showOpts.mnemonic) showOpts.mnemonic = '[hidden]';
this.logger.debug('Creating Wallet:', JSON.stringify(showOpts));
setTimeout(() => {
this.seedWallet(opts).then((walletClient: any) => {
let name = opts.name || this.translate.instant('Personal Wallet');
let myName = opts.myName || this.translate.instant('me');
walletClient.createWallet(name, myName, opts.m, opts.n, {
network: opts.networkName,
singleAddress: opts.singleAddress,
walletPrivKey: opts.walletPrivKey,
coin: opts.coin
}, (err: any, secret: any) => {
if (err) {
this.bwcErrorProvider.cb(err, this.translate.instant('Error creating wallet')).then((msg: string) => {
return reject(msg);
});
} else {
return resolve(walletClient);
}
});
}).catch((err: any) => {
return reject(err);
});
}, 50);
});
}
// create and store a wallet
public createWallet(opts: any): Promise<any> {
return new Promise((resolve, reject) => {
this.doCreateWallet(opts).then((walletClient: any) => {
this.addAndBindWalletClient(walletClient, {
bwsurl: opts.bwsurl
}).then((wallet: any) => {
return resolve(wallet);
});
}).catch((err: any) => {
return reject(err);
});
});
}
// joins and stores a wallet
public joinWallet(opts: any): Promise<any> {
return new Promise((resolve, reject) => {
this.logger.debug('Joining Wallet:', opts);
try {
var walletData = this.bwcProvider.parseSecret(opts.secret);
// check if exist
if (_.find(this.profile.credentials, {
'walletId': walletData.walletId
})) {
return reject(this.translate.instant('Cannot join the same wallet more that once'));
}
} catch (ex) {
this.logger.debug(ex);
return reject(this.translate.instant('Bad wallet invitation'));
}
opts.networkName = walletData.network;
this.logger.debug('Joining Wallet:', opts);
this.seedWallet(opts).then((walletClient: any) => {
walletClient.joinWallet(opts.secret, opts.myName || 'me', {
coin: opts.coin
}, (err: any) => {
if (err) {
this.bwcErrorProvider.cb(err, this.translate.instant('Could not join wallet')).then((msg: string) => {
return reject(msg);
});
} else {
this.addAndBindWalletClient(walletClient, {
bwsurl: opts.bwsurl
}).then((wallet: any) => {
return resolve(wallet);
});
};
});
}).catch((err: any) => {
return reject(err);
});
});
}
public getWallet(walletId: string): any {
return this.wallet[walletId];
};
public deleteWalletClient(wallet: any): Promise<any> {
return new Promise((resolve, reject) => {
let walletId = wallet.credentials.walletId;
this.logger.debug('Deleting Wallet:', wallet.credentials.walletName);
wallet.removeAllListeners();
this.profile.deleteWallet(walletId);
delete this.wallet[walletId];
this.persistenceProvider.removeAllWalletData(walletId).catch((err: any) => {
this.logger.warn(err);
});
this.persistenceProvider.storeProfile(this.profile).then(() => {
return resolve();
}).catch((err: any) => {
return reject(err);
});
});
};
public createDefaultWallet(): Promise<any> {
return new Promise((resolve, reject) => {
let opts: any = {};
opts.m = 1;
opts.n = 1;
opts.networkName = 'livenet';
opts.coin = 'btc';
this.createWallet(opts).then((wallet: any) => {
return resolve(wallet);
}).catch((err) => {
return reject(err);
});
});
};
public setDisclaimerAccepted(): Promise<any> {
return new Promise((resolve, reject) => {
this.profile.disclaimerAccepted = true;
this.persistenceProvider.storeProfile(this.profile).then(() => {
return resolve();
}).catch((err) => {
return reject(err);
});
});
}
public setOnboardingCompleted(): Promise<any> {
return new Promise((resolve, reject) => {
this.profile.onboardingCompleted = true;
this.persistenceProvider.storeProfile(this.profile).then(() => {
return resolve();
}).catch((err) => {
return reject(err);
});
});
}
public getWallets(opts?: any) {
if (opts && !_.isObject(opts)) throw new Error("bad argument");
opts = opts || {};
let ret = _.values(this.wallet);
if (opts.coin) {
ret = _.filter(ret, (x: any) => {
return (x.credentials.coin == opts.coin);
});
}
if (opts.network) {
ret = _.filter(ret, (x: any) => {
return (x.credentials.network == opts.network);
});
}
if (opts.n) {
ret = _.filter(ret, (w: any) => {
return (w.credentials.n == opts.n);
});
}
if (opts.m) {
ret = _.filter(ret, (w: any) => {
return (w.credentials.m == opts.m);
});
}
if (opts.hasFunds) {
ret = _.filter(ret, (w: any) => {
if (!w.status) return;
return (w.status.availableBalanceSat > 0);
});
}
if (opts.minAmount) {
ret = _.filter(ret, (w: any) => {
if (!w.status) return;
return (w.status.availableBalanceSat > opts.minAmount);
});
}
if (opts.onlyComplete) {
ret = _.filter(ret, (w: any) => {
return w.isComplete();
});
} else { }
// Add cached balance async
_.each(ret, (x: any) => {
this.addLastKnownBalance(x);
});
return _.sortBy(ret, 'order');
}
public toggleHideBalanceFlag(walletId: string): void {
this.wallet[walletId].balanceHidden = !this.wallet[walletId].balanceHidden;
this.persistenceProvider.setHideBalanceFlag(walletId, this.wallet[walletId].balanceHidden);
}
public getNotifications(opts: any): Promise<any> {
return new Promise((resolve, reject) => {
opts = opts ? opts : {};
let TIME_STAMP = 60 * 60 * 6;
let MAX = 30;
let typeFilter = {
'NewOutgoingTx': 1,
'NewIncomingTx': 1
};
let w = this.getWallets();
if (_.isEmpty(w)) return reject('Could not find any wallet');
let l = w.length;
let j = 0;
let notifications = [];
let isActivityCached = (wallet: any): boolean => {
return wallet.cachedActivity && wallet.cachedActivity.isValid;
}
let updateNotifications = (wallet: any): Promise<any> => {
return new Promise((resolve, reject) => {
if (isActivityCached(wallet) && !opts.force) {
return resolve();
}
wallet.getNotifications({
timeSpan: TIME_STAMP,
includeOwn: true,
}, (err: any, n: any) => {
if (err) {
return reject(err);
}
wallet.cachedActivity = {
n: n.slice(-MAX),
isValid: true,
};
return resolve();
});
});
}
let process = (notifications: any): any[] => {
if (!notifications) return [];
let shown = _.sortBy(notifications, 'createdOn').reverse();
shown = shown.splice(0, opts.limit || MAX);
_.each(shown, (x: any) => {
x.txpId = x.data ? x.data.txProposalId : null;
x.txid = x.data ? x.data.txid : null;
x.types = [x.type];
x.action = () => {
// TODO?
// $state.go('tabs.wallet', {
// walletId: x.walletId,
// txpId: x.txpId,
// txid: x.txid,
// });
};
});
// let finale = shown; GROUPING DISABLED!
let finale = [];
let prev: any;
// Item grouping... DISABLED.
// REMOVE (if we want 1-to-1 notification) ????
_.each(shown, (x: any) => {
if (prev && prev.walletId === x.walletId && prev.txpId && prev.txpId === x.txpId && prev.creatorId && prev.creatorId === x.creatorId) {
prev.types.push(x.type);
prev.data = _.assign(prev.data, x.data);
prev.txid = prev.txid || x.txid;
prev.creatorName = prev.creatorName || x.creatorName;
} else {
finale.push(x);
prev = x;
}
});
let u = this.bwcProvider.getUtils();
_.each(finale, (x: any) => {
if (x.data && x.data.message && x.wallet && x.wallet.credentials.sharedEncryptingKey) {
// TODO TODO TODO => BWC
x.message = u.decryptMessage(x.data.message, x.wallet.credentials.sharedEncryptingKey);
}
});
return finale;
}
let pr = (wallet, cb) => {
updateNotifications(wallet).then(() => {
let n = _.filter(wallet.cachedActivity.n, (x: any) => {
return typeFilter[x.type];
});
let idToName = {};
if (wallet.cachedStatus) {
_.each(wallet.cachedStatus.wallet.copayers, (c: any) => {
idToName[c.id] = c.name;
});
}
_.each(n, (x: any) => {
x.wallet = wallet;
if (x.creatorId && wallet.cachedStatus) {
x.creatorName = idToName[x.creatorId];
};
});
notifications.push(n);
return cb();
}).catch((err: any) => {
return cb(err);
});
};
_.each(w, (wallet: any) => {
pr(wallet, (err) => {
if (err)
this.logger.warn(this.bwcErrorProvider.msg(err, 'Error updating notifications for ' + wallet.name));
if (++j == l) {
notifications = _.sortBy(notifications, 'createdOn').reverse();
notifications = _.compact(_.flatten(notifications)).slice(0, MAX);
let total = notifications.length;
let processArray = process(notifications);
return resolve({ notifications: processArray, total });
}
});
});
});
}
public getTxps(opts: any): Promise<any> {
return new Promise((resolve, reject) => {
let MAX = 100;
opts = opts ? opts : {};
let w = this.getWallets();
if (_.isEmpty(w)) {
return reject('No wallets available');
}
let txps = [];
_.each(w, (x: any) => {
if (x.pendingTxps)
txps = txps.concat(x.pendingTxps);
});
let n = txps.length;
txps = _.sortBy(txps, 'createdOn').reverse();
txps = _.compact(_.flatten(txps)).slice(0, opts.limit || MAX);
return resolve({ txps, n });
});
};
}