From 51c0c36357e1bc662c4b8d0127e25475e92c2a13 Mon Sep 17 00:00:00 2001 From: Gabriel Masclef Date: Wed, 18 Oct 2017 17:08:13 -0300 Subject: [PATCH] Feat: Profile provider v4 --- src/app/app.component.ts | 4 +- src/models/profile/profile.model.ts | 122 +++ src/pages/home/home.ts | 17 +- src/pages/onboarding/onboarding.ts | 2 +- src/providers/config/config.ts | 24 +- src/providers/profile/profile.ts | 1124 +++++++++++++++++++++++++-- 6 files changed, 1223 insertions(+), 70 deletions(-) create mode 100644 src/models/profile/profile.model.ts diff --git a/src/app/app.component.ts b/src/app/app.component.ts index ccfe45423..bbcfead0f 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -48,7 +48,7 @@ export class CopayApp { this.splashScreen.hide(); } // Check Profile - this.profile.get().then((profile: any) => { + this.profile.loadAndBindProfile().then((profile: any) => { if (profile) { this.logger.info('Profile read. Go to HomePage.'); this.openLockModal(); @@ -58,6 +58,8 @@ export class CopayApp { this.logger.warn('Profile does not exist. Go to Onboarding.'); this.rootPage = OnboardingPage; } + }).catch((err: any) => { + console.log(err); }); }); } diff --git a/src/models/profile/profile.model.ts b/src/models/profile/profile.model.ts new file mode 100644 index 000000000..d1e0a4a6b --- /dev/null +++ b/src/models/profile/profile.model.ts @@ -0,0 +1,122 @@ +import * as moment from 'moment'; + +export class Profile { + + public version: string; + public createdOn: Number; + public credentials: Array; + public disclaimerAccepted: boolean; + public checked: Object; + public checkedUA?: any; + public dirty: boolean; + + constructor() { + this.version = '1.0.0'; + } + + public create(opts?: any): Profile { + opts = opts ? opts : {}; + + let x = new Profile(); + x.createdOn = Date.now(); + x.credentials = opts.credentials || []; + x.disclaimerAccepted = false; + x.checked = {}; + return x; + }; + + public fromObj(obj: any): Profile { + let x = new Profile(); + + x.createdOn = obj.createdOn; + x.credentials = obj.credentials; + x.disclaimerAccepted = obj.disclaimerAccepted; + x.checked = obj.checked || {}; + x.checkedUA = obj.checkedUA || {}; + + if (x.credentials[0] && typeof x.credentials[0] != 'object') + throw ("credentials should be an object"); + + return x; + }; + + public fromString(str: string): Profile { + return this.fromObj(JSON.parse(str)); + }; + + public toObj(): string { + delete this.dirty; + return JSON.stringify(this); + }; + + + public hasWallet(walletId: string): boolean { + for (let i in this.credentials) { + let c = this.credentials[i]; + if (c.walletId == walletId) return true; + }; + return false; + }; + + public isChecked(ua: any, walletId: string): boolean { + return !!(this.checkedUA == ua && this.checked[walletId]); + }; + + + public isDeviceChecked(ua: any): boolean { + return this.checkedUA == ua; + }; + + + public setChecked(ua: any, walletId: string): void { + if (this.checkedUA != ua) { + this.checkedUA = ua; + this.checked = {}; + } + this.checked[walletId] = true; + this.dirty = true; + }; + + public addWallet(credentials: any): boolean { + if (!credentials.walletId) + throw 'credentials must have .walletId'; + + if (this.hasWallet(credentials.walletId)) + return false; + + this.credentials.push(credentials); + this.dirty = true; + return true; + }; + + public updateWallet(credentials: any): boolean { + if (!credentials.walletId) + throw 'credentials must have .walletId'; + + if (!this.hasWallet(credentials.walletId)) + return false; + + this.credentials = this.credentials.map((c: any) => { + if (c.walletId != credentials.walletId) { + return c; + } else { + return credentials + } + }); + + this.dirty = true; + return true; + }; + + public deleteWallet(walletId: string): boolean { + if (!this.hasWallet(walletId)) + return false; + + this.credentials = this.credentials.filter(function (c) { + return c.walletId != walletId; + }); + + this.dirty = true; + return true; + }; +} \ No newline at end of file diff --git a/src/pages/home/home.ts b/src/pages/home/home.ts index 74f7b2ef8..f9b23094f 100644 --- a/src/pages/home/home.ts +++ b/src/pages/home/home.ts @@ -17,19 +17,20 @@ export class HomePage { private release: ReleaseProvider, ) { this.release.getLatestAppVersion() - .catch((err) => { - console.log('Error:', err)}) - .then((version) => { - console.log('Current app version:',version); - var result = this.release.checkForUpdates(version); - console.log('Update available:', result.updateAvailable); - }); + .catch((err) => { + console.log('Error:', err) + }) + .then((version) => { + console.log('Current app version:', version); + var result = this.release.checkForUpdates(version); + console.log('Update available:', result.updateAvailable); + }); } ionViewDidLoad() { console.log('ionViewDidLoad HomePage'); - this.wallets = this.profile.bind(); + this.wallets = this.profile.getWallets(); console.log('[home.ts:20]', this.wallets); //TODO } diff --git a/src/pages/onboarding/onboarding.ts b/src/pages/onboarding/onboarding.ts index e1448aadf..c5d468b47 100644 --- a/src/pages/onboarding/onboarding.ts +++ b/src/pages/onboarding/onboarding.ts @@ -26,7 +26,7 @@ export class OnboardingPage { createProfile() { // TODO: create a new profile - this.profile.create(); + this.profile.createProfile(); } getStarted() { diff --git a/src/providers/config/config.ts b/src/providers/config/config.ts index c382e5e4c..db941e57a 100644 --- a/src/providers/config/config.ts +++ b/src/providers/config/config.ts @@ -4,7 +4,7 @@ import { Events } from 'ionic-angular'; import { PersistenceProvider } from '../persistence/persistence'; import { PlatformProvider } from '../platform/platform'; -import * as _ from "lodash"; +import * as lodash from "lodash"; @Injectable() export class ConfigProvider { @@ -110,30 +110,34 @@ export class ConfigProvider { public load() { return new Promise((resolve, reject) => { this.persistence.getConfig().then((config: object) => { - if (!_.isEmpty(config)) this.configCache = _.clone(config); - else this.configCache = _.clone(this.configDefault); + if (!lodash.isEmpty(config)) this.configCache = lodash.clone(config); + else this.configCache = lodash.clone(this.configDefault); resolve(this.configCache); }); }); } - set(newOpts: object) { - let config = _.cloneDeep(this.configDefault); + public set(newOpts: object) { + let config = lodash.cloneDeep(this.configDefault); - if (_.isString(newOpts)) { + if (lodash.isString(newOpts)) { newOpts = JSON.parse(newOpts); } - _.merge(config, this.configCache, newOpts); + lodash.merge(config, this.configCache, newOpts); this.configCache = config; + this.events.publish('config:updated', this.configCache); + this.persistence.storeConfig(this.configCache).then(() => { this.logger.info('Config saved'); }); - - this.events.publish('config:updated', this.configCache); } - get() { + public get(): Object { return this.configCache; } + public getDefaults(): Object { + return this.configDefault; + } + } diff --git a/src/providers/profile/profile.ts b/src/providers/profile/profile.ts index 0213c2f95..848eb243c 100644 --- a/src/providers/profile/profile.ts +++ b/src/providers/profile/profile.ts @@ -1,81 +1,1105 @@ import { Injectable } from '@angular/core'; -import { Events } from 'ionic-angular'; import { Logger } from '@nsalaun/ng-logger'; -import * as moment from 'moment'; -import * as _ from 'lodash'; +import * as lodash from 'lodash'; import { PersistenceProvider } from '../persistence/persistence'; import { ConfigProvider } from '../config/config'; import { BwcProvider } from '../bwc/bwc'; +import { BwcErrorProvider } from '../bwc-error/bwc-error'; import { WalletProvider } from '../wallet/wallet'; - -interface Profile { - version: string; - createdOn: Number; - credentials: Array; - disclaimerAccepted: boolean; - checked: Object; - checkedUA?: Object; -} - -class Profile implements Profile { - constructor( - public version: string = '1.0.0', - public createdOn: Number = moment( new Date() ).valueOf(), - public credentials: Array = new Array(), - public disclaimerAccepted: boolean = false, - public checked: Object = new Object() - ) { - } - -} +import { PlatformProvider } from '../platform/platform'; +import { AppProvider } from '../../providers/app/app'; +import { LanguageProvider } from '../../providers/language/language'; +import { TxFormatProvider } from '../../providers/tx-format/tx-format'; +import { Profile } from '../../models/profile/profile.model'; @Injectable() 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(); + private queue: Array = []; + constructor( - public events: Events, private logger: Logger, - private wallet: WalletProvider, - private persistence: PersistenceProvider, - private config: ConfigProvider, - private bwc: BwcProvider + private walletProvider: WalletProvider, + private persistenceProvider: PersistenceProvider, + private configProvider: ConfigProvider, + private bwcProvider: BwcProvider, + private bwcErrorProvider: BwcErrorProvider, + private platformProvider: PlatformProvider, + private appProvider: AppProvider, + private languageProvider: LanguageProvider, + private txFormatProvider: TxFormatProvider ) { + this.profile = new Profile; console.log('Hello ProfileProvider Provider'); + this.throttledBwsEvent = lodash.throttle((n, wallet) => { + this.newBwsEvent(n, wallet); + }, 10000); } - get() { + private updateWalletSettings(wallet: any): void { + let config: any = this.configProvider.get(); + let defaults: any = this.configProvider.getDefaults(); + // this.config.whenAvailable(function (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]); + wallet.email = config.emailFor && config.emailFor[wallet.id]; + //}); + } + + public setBackupFlag(walletId: string): void { + this.persistenceProvider.setBackupFlag(walletId).then(() => { + this.logger.debug('Backup flag stored'); + this.wallet[walletId].needsBackup = false; + }).catch((err) => { + if (err) this.logger.error(err); + }); + } + + private requiresBackup(wallet: any): boolean { + if (wallet.isPrivKeyExternal()) return false; + if (!wallet.credentials.mnemonic) return false; + if (wallet.credentials.network == 'testnet') return false; + + return true; + } + + private needsBackup(wallet: any): Promise { return new Promise((resolve, reject) => { - this.persistence.getProfile().then((profile: any) => { - this.profile = profile; - resolve(profile); - }, (error) => { - reject(error); + if (!this.requiresBackup(wallet)) { + return reject(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 balanceIsHidden(wallet: any): Promise { + return new Promise((resolve, reject) => { + + this.persistenceProvider.getHideBalanceFlag(wallet.credentials.walletId).then((shouldHideBalance: string) => { + var hideBalance = (shouldHideBalance == 'true') ? true : false; + return resolve(hideBalance); + }).catch((err) => { + this.logger.error(err); + }); + }); + } + + private bindWalletClient(wallet: any, opts?: any): boolean { + opts = opts ? opts : {}; + var 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; + + this.updateWalletSettings(wallet); + this.wallet[walletId] = wallet; + + this.needsBackup(wallet).then((val: any) => { + wallet.needsBackup = val; + }); + + this.balanceIsHidden(wallet).then((val: any) => { + wallet.balanceHidden = val; + }); + + wallet.removeAllListeners(); + + wallet.on('report', (n: any) => { + this.logger.info('BWC Report:' + n); + }); + + wallet.on('notification', (n: any) => { + this.logger.debug('BWC Notification:', 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())).then(() => { + //$rootScope.$emit('Local/WalletCompleted', walletId); TODO + }); + }); + + 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:' + wallet.status) + }); + }); + + /* TODO $rootScope.$on('Local/SettingsUpdated', (e: any, 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; + + //$rootScope.$emit('bwsEvent', wallet.id, n.type, n); TODO + } + + public updateCredentials(credentials: any): Promise { + return new Promise((resolve, reject) => { + this.profile.updateWallet(credentials); + this.persistenceProvider.storeProfile(this.profile).then(() => { + return resolve(); + }); + }); + } + + public getLastKnownBalance(wid: string): Promise { + 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 { + return new Promise((resolve, reject) => { + let now = Math.floor(Date.now() / 1000); + let showRange = 600; // 10min; + + this.getLastKnownBalance(wallet.id).then((data: string) => { + if (data) { + let parseData: any = JSON.parse(data); + wallet.cachedBalance = parseData.balance; + wallet.cachedBalanceUpdatedOn = (parseData.updatedOn < now - showRange) ? parseData.updatedOn : null; + } + return resolve(); + }).catch((err: any) => { + return reject(err); + }); + }); + } + + public setLastKnownBalance(wid: string, balance: number): Promise { + return new Promise((resolve, reject) => { + this.persistenceProvider.setBalanceCache(wid, { balance: balance, updatedOn: Math.floor(Date.now() / 1000), }); + return resolve(); + }); + } + + 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: 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 { + 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('Could not import. Check input file and spending password'); // TODO getTextCatalog + } + + 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); + }); + }); + } + + // Adds and bind a new client to the profile + private addAndBindWalletClient(wallet: any, opts: any): Promise { + return new Promise((resolve, reject) => { + if (!wallet || !wallet.credentials) { + return reject('Could not access wallet'); // TODO gettextCatalog + } + + let walletId: string = wallet.credentials.walletId + + if (!this.profile.addWallet(JSON.parse(wallet.export()))) { + return reject("Wallet already in " + this.appProvider.info.nameCase); // TODO gettextCatalog + } + + + let skipKeyValidation: boolean = this.shouldSkipValidation(walletId); + if (!skipKeyValidation) + this.runValidation(wallet); + + this.bindWalletClient(wallet); + + let saveBwsUrl = (): Promise => { + 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: 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 { + 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 = lodash.merge(addressBook, localAddressBook1); + this.persistenceProvider.setAddressbook(wallet.credentials.network, JSON.stringify(addressBook)).then(() => { + return resolve(); + }).catch((err: any) => { + return reject(err); + }); + }).catch((err: any) => { + return reject(err); + }); + }); + } + + public importExtendedPrivateKey(xPrivKey: string, opts: any): Promise { + return new Promise((resolve, reject) => { + + var 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, 'Could not import').then((msg: string) => { //TODO getTextCatalog + 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 { + return new Promise((resolve, reject) => { + + var 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, 'Could not import').then((msg: string) => { //TODO getTextCatalog + 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 { + return new Promise((resolve, reject) => { + + var 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, 'Could not import').then((msg: string) => { //TODO getTextCatalog + return reject(msg); + }); + + } + + this.addAndBindWalletClient(walletClient, { + bwsurl: opts.bwsurl + }).then((wallet: any) => { + return resolve(wallet); + }).catch((err: any) => { + return reject(err); + }); + }); + }); + } + + public createProfile(): Promise { + return new Promise((resolve, reject) => { + + this.logger.info('Creating profile'); + let defaults = this.configProvider.getDefaults(); + let config = this.configProvider.get(); + let profile = this.profile.create(); + this.persistenceProvider.storeNewProfile(profile).then((err: any) => { + this.bindProfile(profile).then(() => { + // ignore NONAGREEDDISCLAIMER + return resolve(); + }); + }).catch((err) => { + if (err && err.toString().match('NONAGREEDDISCLAIMER')) { + return resolve(); + } + return reject(err); + }); + }); + } + + public bindProfile(profile: any): Promise { + return new Promise((resolve, reject) => { + this.profile = profile; + + let config = this.configProvider.get(); + + let bindWallets = (): Promise => { + return new Promise((resolve, reject) => { + + let l = this.profile.credentials.length; + let i = 0; + let totalBound = 0; + + if (!l) { + return resolve(); + } + + lodash.each(this.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.isDisclaimerAccepted().then((val) => { + if (!val) { + return reject(new Error('NONAGREEDDISCLAIMER: Non agreed disclaimer')); + } + return resolve(); + }); + }).catch((err: any) => { + return reject(err); + }); + }); + } + + public isDisclaimerAccepted(): Promise { + return new Promise((resolve, reject) => { + + let disclaimerAccepted = this.profile && this.profile.disclaimerAccepted; + if (disclaimerAccepted) return resolve(true); + + // OLD flag + this.persistenceProvider.getCopayDisclaimerFlag().then((val) => { + if (val) { + this.profile.disclaimerAccepted = true; + return resolve(true); + } else { + return reject(); + } + }); + }); + } + + + private bindWallet(credentials: any): Promise { + return new Promise((resolve, reject) => { + + if (!credentials.walletId || !credentials.m) { + return reject('bindWallet should receive credentials JSON'); + } + + // Create the client + let getBWSURL = (walletId: string) => { + var config: any = this.configProvider.get(); + var 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), + }); + + var 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 { + return new Promise((resolve, reject) => { + this.persistenceProvider.getProfile().then((profile: any) => { + if (!profile) { + resolve(profile); + return reject('NOPROFILE: No profile'); + } + // Deprecated: storageService.tryToMigrate + this.logger.debug('Profile read'); + this.bindProfile(profile).then(() => { + return resolve(profile); + }).catch((err: any) => { + return reject(err); + }); + }).catch((err: any) => { + //$rootScope.$emit('Local/DeviceError', err); TODO + return reject(err); + }); + }); + } + + private seedWallet(opts: any): Promise { + 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: network, + passphrase: opts.passphrase, + account: opts.account || 0, + derivationStrategy: opts.derivationStrategy || 'BIP44', + coin: opts.coin + }); + + } catch (ex) { + this.logger.info(ex); + return reject('Could not create: Invalid wallet recovery phrase'); // TODO getTextCatalog + } + } else if (opts.extendedPrivateKey) { + try { + walletClient.seedFromExtendedPrivateKey(opts.extendedPrivateKey, { + network: network, + account: opts.account || 0, + derivationStrategy: opts.derivationStrategy || 'BIP44', + coin: opts.coin, + }); + } catch (ex) { + this.logger.warn(ex); + return reject('Could not create using the specified extended private key'); // TODO GetTextCatalog + } + } 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('Could not create using the specified extended public key'); // TODO GetTextCatalog + } + } else { + let lang = this.languageProvider.getCurrent(); + try { + walletClient.seedFromRandomWithMnemonic({ + network: 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: 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 { + return new Promise((resolve, reject) => { + + let showOpts = lodash.clone(opts); + if (showOpts.extendedPrivateKey) showOpts.extendedPrivateKey = '[hidden]'; + if (showOpts.mnemonic) showOpts.mnemonic = '[hidden]'; + + this.logger.debug('Creating Wallet:', showOpts); + setTimeout(() => { + this.seedWallet(opts).then((walletClient: any) => { + + let name = opts.name || 'Personal Wallet'; // TODO GetTextCatalog + let myName = opts.myName || 'me'; // TODO GetTextCatalog + + 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, 'Error creating wallet').then((msg: string) => { //TODO getTextCatalog + return reject(msg); + }); + } else { + return resolve(walletClient); + } + }); + }).catch((err: any) => { + return reject(err); + }); + }, 50); + }); + } + + // create and store a wallet + public createWallet(opts: any): Promise { + 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 { + return new Promise((resolve, reject) => { + + let walletClient = this.bwcProvider.getClient(null, opts); + this.logger.debug('Joining Wallet:', opts); + + try { + var walletData = this.bwcProvider.parseSecret(opts.secret); + + // check if exist + if (lodash.find(this.profile.credentials, { + 'walletId': walletData.walletId + })) { + return reject('Cannot join the same wallet more that once'); // TODO getTextCatalog + } + } catch (ex) { + this.logger.debug(ex); + return reject('Bad wallet invitation'); // TODO getTextCatalog + } + 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, 'Could not join wallet').then((msg: string) => { //TODO getTextCatalog + 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 { + return new Promise((resolve, reject) => { + + var walletId = wallet.credentials.walletId; + + var config = this.configProvider.get(); + + 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); }); }); }; - create() { - this.profile = new Profile(); + public createDefaultWallet(): Promise { + return new Promise((resolve, reject) => { + var 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); + }); + }); + }; - this.persistence.storeNewProfile(this.profile).then(() => { - this.logger.info('Profile created'); - }, (error) => { - this.logger.error(error); + public setDisclaimerAccepted(): Promise { + return new Promise((resolve, reject) => { + this.profile.disclaimerAccepted = true; + this.persistenceProvider.storeProfile(this.profile).then(() => { + return resolve(); + }).catch((err) => { + return reject(err); + }); }); } - bind() { - let l = this.profile.credentials.length; - let wallets = new Array(); + public getWallets(opts?: any) { - if (!l) return wallets; - let credentials = this.profile.credentials; + if (opts && !lodash.isObject(opts)) throw "bad argument"; - /*_.each(credentials, (credential) => { - wallets.push(this.wallet.bind(credential)); - }); */ - return wallets; + opts = opts || {}; + + let ret = lodash.values(this.wallet); + + if (opts.coin) { + ret = lodash.filter(ret, (x: any) => { + return (x.credentials.coin == opts.coin); + }); + } + + if (opts.network) { + ret = lodash.filter(ret, (x: any) => { + return (x.credentials.network == opts.network); + }); + } + + if (opts.n) { + ret = lodash.filter(ret, (w: any) => { + return (w.credentials.n == opts.n); + }); + } + + if (opts.m) { + ret = lodash.filter(ret, (w: any) => { + return (w.credentials.m == opts.m); + }); + } + + if (opts.hasFunds) { + ret = lodash.filter(ret, (w: any) => { + if (!w.status) return; + return (w.status.availableBalanceSat > 0); + }); + } + + if (opts.minAmount) { + ret = lodash.filter(ret, (w: any) => { + if (!w.status) return; + return (w.status.availableBalanceSat > opts.minAmount); + }); + } + + if (opts.onlyComplete) { + ret = lodash.filter(ret, (w: any) => { + return w.isComplete(); + }); + } else { } + + // Add cached balance async + lodash.each(ret, (x: any) => { + this.addLastKnownBalance(x); + }); + + + return lodash.sortBy(ret, [(x: any) => { + return x.isComplete(); + }, 'createdOn']); } + public toggleHideBalanceFlag(walletId: string): Promise { + return new Promise((resolve, reject) => { + this.wallet[walletId].balanceHidden = !this.wallet[walletId].balanceHidden; + this.persistenceProvider.setHideBalanceFlag(walletId, this.wallet[walletId].balanceHidden.toString()).then(() => { + return resolve(); + }).catch((err: any) => { + return reject(err); + }); + }); + } + + public getNotifications(opts: any): Promise { + 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 (lodash.isEmpty(w)) return resolve(); + + 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 => { + 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): Array => { + if (!notifications) return []; + + let shown = lodash.sortBy(notifications, 'createdOn').reverse(); + + shown = shown.splice(0, opts.limit || MAX); + + lodash.each(shown, (x: any) => { + x.txpId = x.data ? x.data.txProposalId : null; + x.txid = x.data ? x.data.txid : null; + x.types = [x.type]; + + if (x.data && x.data.amount) x.amountStr = this.txFormatProvider.formatAmountStr(x.wallet.coin, x.data.amount); + + x.action = function () { + // 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) ???? + lodash.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 = lodash.assign(prev.data, x.data); + prev.txid = prev.txid || x.txid; + prev.amountStr = prev.amountStr || x.amountStr; + prev.creatorName = prev.creatorName || x.creatorName; + } else { + finale.push(x); + prev = x; + } + }); + + let u = this.bwcProvider.getUtils(); + lodash.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; + } + + lodash.each(w, (wallet: any) => { + updateNotifications(wallet).then(() => { + j++; + let n = lodash.filter(wallet.cachedActivity.n, (x: any) => { + return typeFilter[x.type]; + }); + + let idToName = {}; + if (wallet.cachedStatus) { + lodash.each(wallet.cachedStatus.wallet.copayers, (c: any) => { + idToName[c.id] = c.name; + }); + } + + lodash.each(n, (x: any) => { + x.wallet = wallet; + if (x.creatorId && wallet.cachedStatus) { + x.creatorName = idToName[x.creatorId]; + }; + }); + + notifications.push(n); + + if (j == l) { + notifications = lodash.sortBy(notifications, 'createdOn'); + notifications = lodash.compact(lodash.flatten(notifications)).slice(0, MAX); + let total = notifications.length; + return resolve({ processArray: process(notifications), total: total }); + }; + }).catch((err: any) => { + this.logger.warn('Error updating notifications:' + err); + }); + }); + }); + } + + public getTxps(opts: any): Promise { + return new Promise((resolve, reject) => { + let MAX = 100; + opts = opts ? opts : {}; + + let w = this.getWallets(); + if (lodash.isEmpty(w)) { + return reject('No wallets available'); + } + + let txps = []; + + lodash.each(w, (x: any) => { + if (x.pendingTxps) + txps = txps.concat(x.pendingTxps); + }); + let n = txps.length; + txps = lodash.sortBy(txps, 'pendingForUs', 'createdOn'); + txps = lodash.compact(lodash.flatten(txps)).slice(0, opts.limit || MAX); + return resolve({ txps: txps, n: n }); + }); + }; + }