From c975347b6f0a9276819e338cfd060b1a2cebc99e Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Date: Thu, 26 Dec 2019 20:21:07 -0600 Subject: [PATCH] REF: Reworked Import wallet flow --- BlueComponents.js | 241 ++++++++++++------- MainBottomTabs.js | 4 - class/app-storage.js | 13 +- class/index.js | 1 + class/placeholder-wallet.js | 31 +++ class/walletGradient.js | 4 + class/walletImport.js | 228 ++++++++++++++++++ screen/wallets/import.js | 399 ++++++++----------------------- screen/wallets/list.js | 62 ++++- screen/wallets/reorderWallets.js | 4 +- screen/wallets/scanQrWif.js | 344 -------------------------- 11 files changed, 581 insertions(+), 750 deletions(-) create mode 100644 class/placeholder-wallet.js create mode 100644 class/walletImport.js delete mode 100644 screen/wallets/scanQrWif.js diff --git a/BlueComponents.js b/BlueComponents.js index 2a5d577a3..5355b6c41 100644 --- a/BlueComponents.js +++ b/BlueComponents.js @@ -23,7 +23,7 @@ import { TextInput, } from 'react-native'; import LinearGradient from 'react-native-linear-gradient'; -import { LightningCustodianWallet } from './class'; +import { LightningCustodianWallet, PlaceholderWallet } from './class'; import Carousel from 'react-native-snap-carousel'; import { BitcoinUnit } from './models/bitcoinUnits'; import NavigationService from './NavigationService'; @@ -748,15 +748,17 @@ export class BlueHeaderDefaultMain extends Component { } rightComponent={ - - - + this.props.onNewWalletPress && ( + + + + ) } /> @@ -957,7 +959,7 @@ export class BlueLoading extends Component { render() { return ( - + @@ -1844,97 +1846,166 @@ export class WalletsCarousel extends Component { ); } - return ( - - { - if (WalletsCarousel.handleClick) { - WalletsCarousel.handleClick(index); - } - }} + if (item.type === PlaceholderWallet.type) { + return ( + - { + if (item.getIsFailure() && WalletsCarousel.handleClick) { + WalletsCarousel.handleClick(index); + } }} > - - - - - {item.getLabel()} - - {item.hideBalance ? ( - - ) : ( + + - {loc.formatBalance(Number(item.getBalance()), item.getPreferredBalanceUnit(), true)} + {item.getLabel()} - )} - - + An error was encountered when attempting to import this wallet. + + ) : ( + + )} + + + + ); + } else { + return ( + + { + if (WalletsCarousel.handleClick) { + WalletsCarousel.handleClick(index); + } + }} + > + - {loc.wallets.list.latest_transaction} - - - {loc.transactionTimeToReadable(item.getLatestTransactionTime())} - - - - - ); + + + + + {item.getLabel()} + + {item.hideBalance ? ( + + ) : ( + + {loc.formatBalance(Number(item.getBalance()), item.getPreferredBalanceUnit(), true)} + + )} + + + {loc.wallets.list.latest_transaction} + + + {loc.transactionTimeToReadable(item.getLatestTransactionTime())} + + + + + ); + } } snapToItem = item => { diff --git a/MainBottomTabs.js b/MainBottomTabs.js index 03ffee145..aadd42e69 100644 --- a/MainBottomTabs.js +++ b/MainBottomTabs.js @@ -23,7 +23,6 @@ import WalletExport from './screen/wallets/export'; import WalletXpub from './screen/wallets/xpub'; import BuyBitcoin from './screen/wallets/buyBitcoin'; import Marketplace from './screen/wallets/marketplace'; -import scanQrWif from './screen/wallets/scanQrWif'; import ReorderWallets from './screen/wallets/reorderWallets'; import SelectWallet from './screen/wallets/selectWallet'; @@ -277,9 +276,6 @@ const MainBottomTabs = createStackNavigator( header: null, }, }, - ScanQrWif: { - screen: scanQrWif, - }, WalletExport: { screen: WalletExport, }, diff --git a/class/app-storage.js b/class/app-storage.js index bdba64a15..df9f9aace 100644 --- a/class/app-storage.js +++ b/class/app-storage.js @@ -9,8 +9,9 @@ import { SegwitP2SHWallet, SegwitBech32Wallet, HDSegwitBech32Wallet, + PlaceholderWallet, + LightningCustodianWallet, } from './'; -import { LightningCustodianWallet } from './lightning-custodian-wallet'; import WatchConnectivity from '../WatchConnectivity'; import DeviceQuickActions from './quickActions'; const encryption = require('../encryption'); @@ -192,6 +193,8 @@ export class AppStorage { let tempObj = JSON.parse(key); let unserializedWallet; switch (tempObj.type) { + case PlaceholderWallet.type: + continue; case SegwitBech32Wallet.type: unserializedWallet = SegwitBech32Wallet.fromJson(key); break; @@ -298,7 +301,7 @@ export class AppStorage { async saveToDisk() { let walletsToSave = []; for (let key of this.wallets) { - if (typeof key === 'boolean') continue; + if (typeof key === 'boolean' || key.type === PlaceholderWallet.type) continue; if (key.prepareForSerialization) key.prepareForSerialization(); walletsToSave.push(JSON.stringify({ ...key, type: key.type })); } @@ -349,13 +352,13 @@ export class AppStorage { console.log('fetchWalletBalances for wallet#', index); if (index || index === 0) { let c = 0; - for (let wallet of this.wallets) { + for (let wallet of this.wallets.filter(wallet => wallet.type !== PlaceholderWallet.type)) { if (c++ === index) { await wallet.fetchBalance(); } } } else { - for (let wallet of this.wallets) { + for (let wallet of this.wallets.filter(wallet => wallet.type !== PlaceholderWallet.type)) { await wallet.fetchBalance(); } } @@ -375,7 +378,7 @@ export class AppStorage { console.log('fetchWalletTransactions for wallet#', index); if (index || index === 0) { let c = 0; - for (let wallet of this.wallets) { + for (let wallet of this.wallets.filter(wallet => wallet.type !== PlaceholderWallet.type)) { if (c++ === index) { await wallet.fetchTransactions(); if (wallet.fetchPendingTransactions) { diff --git a/class/index.js b/class/index.js index c6add4c07..1ec5ece22 100644 --- a/class/index.js +++ b/class/index.js @@ -12,3 +12,4 @@ export * from './lightning-custodian-wallet'; export * from './abstract-hd-wallet'; export * from './hd-segwit-bech32-wallet'; export * from './hd-segwit-bech32-transaction'; +export * from './placeholder-wallet'; diff --git a/class/placeholder-wallet.js b/class/placeholder-wallet.js new file mode 100644 index 000000000..206b3b59c --- /dev/null +++ b/class/placeholder-wallet.js @@ -0,0 +1,31 @@ +import { AbstractWallet } from './abstract-wallet'; + +export class PlaceholderWallet extends AbstractWallet { + static type = 'placeholder'; + static typeReadable = 'Placeholder'; + + constructor() { + super(); + this._isFailure = false; + } + + allowSend() { + return false; + } + + getLabel() { + return this.getIsFailure() ? 'Wallet Import' : 'Importing Wallet...'; + } + + allowReceive() { + return false; + } + + getIsFailure() { + return this._isFailure; + } + + setIsFailure(value) { + this._isFailure = value; + } +} diff --git a/class/walletGradient.js b/class/walletGradient.js index 46c4d2ef8..a861a4ef8 100644 --- a/class/walletGradient.js +++ b/class/walletGradient.js @@ -5,6 +5,7 @@ import { HDLegacyBreadwalletWallet } from './hd-legacy-breadwallet-wallet'; import { HDLegacyP2PKHWallet } from './hd-legacy-p2pkh-wallet'; import { WatchOnlyWallet } from './watch-only-wallet'; import { HDSegwitBech32Wallet } from './hd-segwit-bech32-wallet'; +import { PlaceholderWallet } from './placeholder-wallet'; export default class WalletGradient { static hdSegwitP2SHWallet = ['#65ceef', '#68bbe1']; @@ -41,6 +42,9 @@ export default class WalletGradient { case LightningCustodianWallet.type: gradient = WalletGradient.lightningCustodianWallet; break; + case PlaceholderWallet.type: + gradient = WalletGradient.watchOnlyWallet; + break; case 'CreateWallet': gradient = WalletGradient.createWallet; break; diff --git a/class/walletImport.js b/class/walletImport.js new file mode 100644 index 000000000..969506d53 --- /dev/null +++ b/class/walletImport.js @@ -0,0 +1,228 @@ +/* global alert */ +import { + SegwitP2SHWallet, + LegacyWallet, + WatchOnlyWallet, + HDLegacyBreadwalletWallet, + HDSegwitP2SHWallet, + HDLegacyP2PKHWallet, + HDSegwitBech32Wallet, + LightningCustodianWallet, + PlaceholderWallet, +} from '../class'; +import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; +const EV = require('../events'); +const A = require('../analytics'); +/** @type {AppStorage} */ +const BlueApp = require('../BlueApp'); +const loc = require('../loc'); + +export default class WalletImport { + static async _saveWallet(w) { + try { + const wallet = BlueApp.getWallets().some(wallet => wallet.getSecret() === w.secret && wallet.type !== PlaceholderWallet.type); + if (wallet) { + alert('This wallet has been previously imported.'); + WalletImport.removePlaceholderWallet(); + } else { + alert(loc.wallets.import.success); + ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false }); + w.setLabel(loc.wallets.import.imported + ' ' + w.typeReadable); + WalletImport.removePlaceholderWallet(); + BlueApp.wallets.push(w); + await BlueApp.saveToDisk(); + A(A.ENUM.CREATED_WALLET); + } + EV(EV.enum.WALLETS_COUNT_CHANGED); + } catch (_e) {} + } + + static removePlaceholderWallet() { + const placeholderWalletIndex = BlueApp.wallets.findIndex(wallet => wallet.type === PlaceholderWallet.type); + if (placeholderWalletIndex > -1) { + BlueApp.wallets.splice(placeholderWalletIndex, 1); + } + } + + static addPlaceholderWallet(importText, isFailure = false) { + const wallet = new PlaceholderWallet(); + wallet.setSecret(importText); + wallet.setIsFailure(isFailure); + BlueApp.wallets.push(wallet); + EV(EV.enum.WALLETS_COUNT_CHANGED); + return wallet; + } + + static isCurrentlyImportingWallet() { + return BlueApp.getWallets().some(wallet => wallet.type === PlaceholderWallet.type); + } + + static async processImportText(importText) { + if (WalletImport.isCurrentlyImportingWallet()) { + return; + } + const placeholderWallet = WalletImport.addPlaceholderWallet(importText); + // Plan: + // 0. check if its HDSegwitBech32Wallet (BIP84) + // 1. check if its HDSegwitP2SHWallet (BIP49) + // 2. check if its HDLegacyP2PKHWallet (BIP44) + // 3. check if its HDLegacyBreadwalletWallet (no BIP, just "m/0") + // 4. check if its Segwit WIF (P2SH) + // 5. check if its Legacy WIF + // 6. check if its address (watch-only wallet) + // 7. check if its private key (segwit address P2SH) TODO + // 7. check if its private key (legacy address) TODO + + try { + // is it lightning custodian? + if (importText.indexOf('blitzhub://') !== -1 || importText.indexOf('lndhub://') !== -1) { + let lnd = new LightningCustodianWallet(); + if (importText.includes('@')) { + const split = importText.split('@'); + lnd.setBaseURI(split[1]); + lnd.setSecret(split[0]); + } else { + lnd.setBaseURI(LightningCustodianWallet.defaultBaseUri); + lnd.setSecret(importText); + } + lnd.init(); + await lnd.authorize(); + await lnd.fetchTransactions(); + await lnd.fetchUserInvoices(); + await lnd.fetchPendingTransactions(); + await lnd.fetchBalance(); + return WalletImport._saveWallet(lnd); + } + + // trying other wallet types + + let hd4 = new HDSegwitBech32Wallet(); + hd4.setSecret(importText); + if (hd4.validateMnemonic()) { + await hd4.fetchBalance(); + if (hd4.getBalance() > 0) { + await hd4.fetchTransactions(); + return WalletImport._saveWallet(hd4); + } + } + + let segwitWallet = new SegwitP2SHWallet(); + segwitWallet.setSecret(importText); + if (segwitWallet.getAddress()) { + // ok its a valid WIF + + let legacyWallet = new LegacyWallet(); + legacyWallet.setSecret(importText); + + await legacyWallet.fetchBalance(); + if (legacyWallet.getBalance() > 0) { + // yep, its legacy we're importing + await legacyWallet.fetchTransactions(); + return WalletImport._saveWallet(legacyWallet); + } else { + // by default, we import wif as Segwit P2SH + await segwitWallet.fetchBalance(); + await segwitWallet.fetchTransactions(); + return WalletImport._saveWallet(segwitWallet); + } + } + + // case - WIF is valid, just has uncompressed pubkey + + let legacyWallet = new LegacyWallet(); + legacyWallet.setSecret(importText); + if (legacyWallet.getAddress()) { + await legacyWallet.fetchBalance(); + await legacyWallet.fetchTransactions(); + return WalletImport._saveWallet(legacyWallet); + } + + // if we're here - nope, its not a valid WIF + + let hd1 = new HDLegacyBreadwalletWallet(); + hd1.setSecret(importText); + if (hd1.validateMnemonic()) { + await hd1.fetchBalance(); + if (hd1.getBalance() > 0) { + await hd1.fetchTransactions(); + return WalletImport._saveWallet(hd1); + } + } + + let hd2 = new HDSegwitP2SHWallet(); + hd2.setSecret(importText); + if (hd2.validateMnemonic()) { + await hd2.fetchBalance(); + if (hd2.getBalance() > 0) { + await hd2.fetchTransactions(); + return WalletImport._saveWallet(hd2); + } + } + + let hd3 = new HDLegacyP2PKHWallet(); + hd3.setSecret(importText); + if (hd3.validateMnemonic()) { + await hd3.fetchBalance(); + if (hd3.getBalance() > 0) { + await hd3.fetchTransactions(); + return WalletImport._saveWallet(hd3); + } + } + + // no balances? how about transactions count? + + if (hd1.validateMnemonic()) { + await hd1.fetchTransactions(); + if (hd1.getTransactions().length !== 0) { + return WalletImport._saveWallet(hd1); + } + } + if (hd2.validateMnemonic()) { + await hd2.fetchTransactions(); + if (hd2.getTransactions().length !== 0) { + return WalletImport._saveWallet(hd2); + } + } + if (hd3.validateMnemonic()) { + await hd3.fetchTransactions(); + if (hd3.getTransactions().length !== 0) { + return WalletImport._saveWallet(hd3); + } + } + if (hd4.validateMnemonic()) { + await hd4.fetchTransactions(); + if (hd4.getTransactions().length !== 0) { + return WalletImport._saveWallet(hd4); + } + } + + // is it even valid? if yes we will import as: + if (hd4.validateMnemonic()) { + return WalletImport._saveWallet(hd4); + } + + // not valid? maybe its a watch-only address? + + let watchOnly = new WatchOnlyWallet(); + watchOnly.setSecret(importText); + if (watchOnly.valid()) { + await watchOnly.fetchTransactions(); + await watchOnly.fetchBalance(); + return WalletImport._saveWallet(watchOnly); + } + + // nope? + + // TODO: try a raw private key + } catch (Err) { + WalletImport.removePlaceholderWallet(placeholderWallet); + EV(EV.enum.WALLETS_COUNT_CHANGED); + console.warn(Err); + } + WalletImport.removePlaceholderWallet(); + WalletImport.addPlaceholderWallet(importText, true); + ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); + EV(EV.enum.WALLETS_COUNT_CHANGED); + alert(loc.wallets.import.error); + } +} diff --git a/screen/wallets/import.js b/screen/wallets/import.js index ab590a75c..86ecbe568 100644 --- a/screen/wallets/import.js +++ b/screen/wallets/import.js @@ -1,326 +1,125 @@ /* global alert */ -import { - SegwitP2SHWallet, - LegacyWallet, - WatchOnlyWallet, - HDLegacyBreadwalletWallet, - HDSegwitP2SHWallet, - HDLegacyP2PKHWallet, - HDSegwitBech32Wallet, -} from '../../class'; -import React, { Component } from 'react'; +import React, { useEffect, useState } from 'react'; import { KeyboardAvoidingView, Platform, Dimensions, View, TouchableWithoutFeedback, Keyboard } from 'react-native'; import { BlueFormMultiInput, BlueButtonLink, BlueFormLabel, - BlueLoading, BlueDoneAndDismissKeyboardInputAccessory, BlueButton, SafeBlueArea, BlueSpacing20, BlueNavigationStyle, } from '../../BlueComponents'; -import PropTypes from 'prop-types'; -import { LightningCustodianWallet } from '../../class/lightning-custodian-wallet'; import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; import Privacy from '../../Privacy'; -let EV = require('../../events'); -let A = require('../../analytics'); -/** @type {AppStorage} */ -let BlueApp = require('../../BlueApp'); +import { useNavigation, useNavigationParam } from 'react-navigation-hooks'; +import WalletImport from '../../class/walletImport'; let loc = require('../../loc'); const { width } = Dimensions.get('window'); -export default class WalletsImport extends Component { - static navigationOptions = { - ...BlueNavigationStyle(), - title: loc.wallets.import.title, +const WalletsImport = () => { + const [isToolbarVisibleForAndroid, setIsToolbarVisibleForAndroid] = useState(false); + const [importText, setImportText] = useState(useNavigationParam('label') || ''); + const { navigate, dismiss } = useNavigation(); + + useEffect(() => { + Privacy.enableBlur(); + return () => Privacy.disableBlur(); + }); + + const importButtonPressed = () => { + if (importText.trim().length === 0) { + return; + } + importMnemonic(importText); }; - constructor(props) { - super(props); - this.state = { - isLoading: true, - isToolbarVisibleForAndroid: false, - }; - } - - componentDidMount() { - this.setState({ - isLoading: false, - label: '', - }); - Privacy.enableBlur(); - } - - componentWillUnmount() { - Privacy.disableBlur(); - } - - async _saveWallet(w) { - if (BlueApp.getWallets().some(wallet => wallet.getSecret() === w.secret)) { - alert('This wallet has been previously imported.'); - } else { - alert(loc.wallets.import.success); - ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false }); - w.setLabel(loc.wallets.import.imported + ' ' + w.typeReadable); - w.setUserHasSavedExport(true); - BlueApp.wallets.push(w); - await BlueApp.saveToDisk(); - EV(EV.enum.WALLETS_COUNT_CHANGED); - A(A.ENUM.CREATED_WALLET); - this.props.navigation.dismiss(); - } - } - - async importMnemonic(text) { + const importMnemonic = importText => { try { - // is it lightning custodian? - if (text.indexOf('blitzhub://') !== -1 || text.indexOf('lndhub://') !== -1) { - let lnd = new LightningCustodianWallet(); - if (text.includes('@')) { - const split = text.split('@'); - lnd.setBaseURI(split[1]); - lnd.setSecret(split[0]); - } else { - lnd.setBaseURI(LightningCustodianWallet.defaultBaseUri); - lnd.setSecret(text); - } - lnd.init(); - await lnd.authorize(); - await lnd.fetchTransactions(); - await lnd.fetchUserInvoices(); - await lnd.fetchPendingTransactions(); - await lnd.fetchBalance(); - return this._saveWallet(lnd); - } - - // trying other wallet types - - let hd4 = new HDSegwitBech32Wallet(); - hd4.setSecret(text); - if (hd4.validateMnemonic()) { - await hd4.fetchBalance(); - if (hd4.getBalance() > 0) { - await hd4.fetchTransactions(); - return this._saveWallet(hd4); - } - } - - let segwitWallet = new SegwitP2SHWallet(); - segwitWallet.setSecret(text); - if (segwitWallet.getAddress()) { - // ok its a valid WIF - - let legacyWallet = new LegacyWallet(); - legacyWallet.setSecret(text); - - await legacyWallet.fetchBalance(); - if (legacyWallet.getBalance() > 0) { - // yep, its legacy we're importing - await legacyWallet.fetchTransactions(); - return this._saveWallet(legacyWallet); - } else { - // by default, we import wif as Segwit P2SH - await segwitWallet.fetchBalance(); - await segwitWallet.fetchTransactions(); - return this._saveWallet(segwitWallet); - } - } - - // case - WIF is valid, just has uncompressed pubkey - - let legacyWallet = new LegacyWallet(); - legacyWallet.setSecret(text); - if (legacyWallet.getAddress()) { - await legacyWallet.fetchBalance(); - await legacyWallet.fetchTransactions(); - return this._saveWallet(legacyWallet); - } - - // if we're here - nope, its not a valid WIF - - let hd1 = new HDLegacyBreadwalletWallet(); - hd1.setSecret(text); - if (hd1.validateMnemonic()) { - await hd1.fetchBalance(); - if (hd1.getBalance() > 0) { - await hd1.fetchTransactions(); - return this._saveWallet(hd1); - } - } - - let hd2 = new HDSegwitP2SHWallet(); - hd2.setSecret(text); - if (hd2.validateMnemonic()) { - await hd2.fetchBalance(); - if (hd2.getBalance() > 0) { - await hd2.fetchTransactions(); - return this._saveWallet(hd2); - } - } - - let hd3 = new HDLegacyP2PKHWallet(); - hd3.setSecret(text); - if (hd3.validateMnemonic()) { - await hd3.fetchBalance(); - if (hd3.getBalance() > 0) { - await hd3.fetchTransactions(); - return this._saveWallet(hd3); - } - } - - // no balances? how about transactions count? - - if (hd1.validateMnemonic()) { - await hd1.fetchTransactions(); - if (hd1.getTransactions().length !== 0) { - return this._saveWallet(hd1); - } - } - if (hd2.validateMnemonic()) { - await hd2.fetchTransactions(); - if (hd2.getTransactions().length !== 0) { - return this._saveWallet(hd2); - } - } - if (hd3.validateMnemonic()) { - await hd3.fetchTransactions(); - if (hd3.getTransactions().length !== 0) { - return this._saveWallet(hd3); - } - } - if (hd4.validateMnemonic()) { - await hd4.fetchTransactions(); - if (hd4.getTransactions().length !== 0) { - return this._saveWallet(hd4); - } - } - - // is it even valid? if yes we will import as: - if (hd4.validateMnemonic()) { - return this._saveWallet(hd4); - } - - // not valid? maybe its a watch-only address? - - let watchOnly = new WatchOnlyWallet(); - watchOnly.setSecret(text); - if (watchOnly.valid()) { - await watchOnly.fetchTransactions(); - await watchOnly.fetchBalance(); - return this._saveWallet(watchOnly); - } - - // nope? - - // TODO: try a raw private key - } catch (Err) { - console.warn(Err); + WalletImport.processImportText(importText); + dismiss(); + } catch (error) { + alert(loc.wallets.import.error); + ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); } + }; - alert(loc.wallets.import.error); - ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); - // Plan: - // 0. check if its HDSegwitBech32Wallet (BIP84) - // 1. check if its HDSegwitP2SHWallet (BIP49) - // 2. check if its HDLegacyP2PKHWallet (BIP44) - // 3. check if its HDLegacyBreadwalletWallet (no BIP, just "m/0") - // 4. check if its Segwit WIF (P2SH) - // 5. check if its Legacy WIF - // 6. check if its address (watch-only wallet) - // 7. check if its private key (segwit address P2SH) TODO - // 7. check if its private key (legacy address) TODO - } + const onBarScanned = value => { + setImportText(value); + importMnemonic(value); + }; - setLabel(text) { - this.setState({ - label: text, - }); /* also, a hack to make screen update new typed text */ - } + return ( + + + + {loc.wallets.import.explanation} + + setIsToolbarVisibleForAndroid(true)} + onBlur={() => setIsToolbarVisibleForAndroid(false)} + /> + {Platform.select({ + ios: ( + { + setImportText(''); + Keyboard.dismiss(); + }} + onPasteTapped={text => { + setImportText(text); + Keyboard.dismiss(); + }} + /> + ), + android: isToolbarVisibleForAndroid && ( + { + setImportText(''); + Keyboard.dismiss(); + }} + onPasteTapped={text => { + setImportText(text); + Keyboard.dismiss(); + }} + /> + ), + })} + + - render() { - if (this.state.isLoading) { - return ( - - - - ); - } - - return ( - - - - {loc.wallets.import.explanation} - - { - this.setLabel(text); - }} - inputAccessoryViewID={BlueDoneAndDismissKeyboardInputAccessory.InputAccessoryViewID} - onFocus={() => this.setState({ isToolbarVisibleForAndroid: true })} - onBlur={() => this.setState({ isToolbarVisibleForAndroid: false })} - /> - {Platform.select({ - ios: ( - this.setState({ label: '' }, () => Keyboard.dismiss())} - onPasteTapped={text => this.setState({ label: text }, () => Keyboard.dismiss())} - /> - ), - android: this.state.isToolbarVisibleForAndroid && ( - this.setState({ label: '' }, () => Keyboard.dismiss())} - onPasteTapped={text => this.setState({ label: text }, () => Keyboard.dismiss())} - /> - ), - })} - - - - - + + - { - if (!this.state.label) { - return; - } - this.setState({ isLoading: true }, async () => { - await this.importMnemonic(this.state.label.trim()); - this.setState({ isLoading: false }); - }); - }} - /> - { - this.props.navigation.navigate('ScanQrWif'); - }} - /> - - - ); - } -} - -WalletsImport.propTypes = { - navigation: PropTypes.shape({ - navigate: PropTypes.func, - goBack: PropTypes.func, - dismiss: PropTypes.func, - }), + onPress={importButtonPressed} + /> + { + navigate('ScanQrAddress', { onBarScanned }); + }} + /> + + + ); }; + +WalletsImport.navigationOptions = { + ...BlueNavigationStyle(), + title: loc.wallets.import.title, +}; +export default WalletsImport; diff --git a/screen/wallets/list.js b/screen/wallets/list.js index 43c506b50..2c400cfd1 100644 --- a/screen/wallets/list.js +++ b/screen/wallets/list.js @@ -1,11 +1,13 @@ /* global alert */ import React, { Component } from 'react'; -import { View, TouchableOpacity, Text, FlatList, InteractionManager, RefreshControl, ScrollView } from 'react-native'; +import { View, TouchableOpacity, Text, FlatList, InteractionManager, RefreshControl, ScrollView, Alert } from 'react-native'; import { BlueLoading, SafeBlueArea, WalletsCarousel, BlueList, BlueHeaderDefaultMain, BlueTransactionListItem } from '../../BlueComponents'; import { Icon } from 'react-native-elements'; import { NavigationEvents } from 'react-navigation'; import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; import PropTypes from 'prop-types'; +import { PlaceholderWallet } from '../../class'; +import WalletImport from '../../class/walletImport'; let EV = require('../../events'); let A = require('../../analytics'); /** @type {AppStorage} */ @@ -135,7 +137,7 @@ export default class WalletsList extends Component { }, () => { if (scrollToEnd) { - this.walletsCarousel.snapToItem(this.state.wallets.length - 1); + this.walletsCarousel.snapToItem(this.state.wallets.length - 2); } }, ); @@ -152,13 +154,42 @@ export default class WalletsList extends Component { console.log('click', index); let wallet = BlueApp.wallets[index]; if (wallet) { - this.props.navigation.navigate('WalletTransactions', { - wallet: wallet, - key: `WalletTransactions-${wallet.getID()}`, - }); + if (wallet.type === PlaceholderWallet.type) { + Alert.alert( + loc.wallets.add.details, + 'There was a problem importing this wallet.', + [ + { + text: loc.wallets.details.delete, + onPress: () => { + WalletImport.removePlaceholderWallet(); + EV(EV.enum.WALLETS_COUNT_CHANGED); + }, + style: 'destructive', + }, + { + text: 'Try Again', + onPress: () => { + this.props.navigation.navigate('ImportWallet', { label: wallet.getSecret() }); + WalletImport.removePlaceholderWallet(); + EV(EV.enum.WALLETS_COUNT_CHANGED); + }, + style: 'default', + }, + ], + { cancelable: false }, + ); + } else { + this.props.navigation.navigate('WalletTransactions', { + wallet: wallet, + key: `WalletTransactions-${wallet.getID()}`, + }); + } } else { // if its out of index - this must be last card with incentive to create wallet - this.props.navigation.navigate('AddWallet'); + if (!BlueApp.getWallets().some(wallet => wallet.type === PlaceholderWallet.type)) { + this.props.navigation.navigate('AddWallet'); + } } } @@ -171,6 +202,10 @@ export default class WalletsList extends Component { // not the last } + if (this.state.wallets[index].type === PlaceholderWallet.type) { + return; + } + // now, lets try to fetch balance and txs for this wallet in case it has changed this.lazyRefreshWallet(index); } @@ -193,7 +228,7 @@ export default class WalletsList extends Component { let didRefresh = false; try { - if (wallets && wallets[index] && wallets[index].timeToRefreshBalance()) { + if (wallets && wallets[index] && wallets[index].type !== PlaceholderWallet.type && wallets[index].timeToRefreshBalance()) { console.log('snapped to, and now its time to refresh wallet #', index); await wallets[index].fetchBalance(); if (oldBalance !== wallets[index].getBalance() || wallets[index].getUnconfirmedBalance() !== 0) { @@ -250,7 +285,7 @@ export default class WalletsList extends Component { }; handleLongPress = () => { - if (BlueApp.getWallets().length > 1) { + if (BlueApp.getWallets().length > 1 && !BlueApp.getWallets().some(wallet => wallet.type === PlaceholderWallet.type)) { this.props.navigation.navigate('ReorderWallets'); } else { ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); @@ -276,7 +311,14 @@ export default class WalletsList extends Component { this.refreshTransactions()} refreshing={!this.state.isFlatListRefreshControlHidden} /> } > - this.props.navigation.navigate('AddWallet')} /> + wallet.type === PlaceholderWallet.type) + ? () => this.props.navigation.navigate('AddWallet') + : null + } + /> wallet.type !== PlaceholderWallet.type); this.setState({ data: wallets, isLoading: false, diff --git a/screen/wallets/scanQrWif.js b/screen/wallets/scanQrWif.js deleted file mode 100644 index a295d2085..000000000 --- a/screen/wallets/scanQrWif.js +++ /dev/null @@ -1,344 +0,0 @@ -/* global alert */ -import React from 'react'; -import { ActivityIndicator, Image, View, TouchableOpacity } from 'react-native'; -import { BlueText, SafeBlueArea, BlueButton } from '../../BlueComponents'; -import { RNCamera } from 'react-native-camera'; -import { SegwitP2SHWallet, LegacyWallet, WatchOnlyWallet, HDLegacyP2PKHWallet, HDSegwitBech32Wallet } from '../../class'; -import PropTypes from 'prop-types'; -import { HDSegwitP2SHWallet } from '../../class/hd-segwit-p2sh-wallet'; -import { LightningCustodianWallet } from '../../class/lightning-custodian-wallet'; -import bip21 from 'bip21'; -/** @type {AppStorage} */ -let BlueApp = require('../../BlueApp'); -let EV = require('../../events'); -let bip38 = require('../../bip38'); -let wif = require('wif'); -let prompt = require('../../prompt'); -let loc = require('../../loc'); - -export default class ScanQrWif extends React.Component { - static navigationOptions = { - header: null, - }; - - state = { isLoading: false }; - - onBarCodeScanned = async ret => { - if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.pausePreview(); - if (+new Date() - this.lastTimeIveBeenHere < 6000) { - this.lastTimeIveBeenHere = +new Date(); - return; - } - this.lastTimeIveBeenHere = +new Date(); - this.setState({ isLoading: true }); - if (ret.data[0] === '6') { - // password-encrypted, need to ask for password and decrypt - console.log('trying to decrypt...'); - - this.setState({ - message: loc.wallets.scanQrWif.decoding, - }); - shold_stop_bip38 = undefined; // eslint-disable-line - let password = await prompt(loc.wallets.scanQrWif.input_password, loc.wallets.scanQrWif.password_explain); - if (!password) { - return; - } - let that = this; - try { - let decryptedKey = await bip38.decrypt(ret.data, password, function(status) { - that.setState({ - message: loc.wallets.scanQrWif.decoding + '... ' + status.percent.toString().substr(0, 4) + ' %', - }); - }); - ret.data = wif.encode(0x80, decryptedKey.privateKey, decryptedKey.compressed); - } catch (e) { - console.log(e.message); - if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.resumePreview(); - this.setState({ message: false, isLoading: false }); - return alert(loc.wallets.scanQrWif.bad_password); - } - - this.setState({ message: false, isLoading: false }); - } - - for (let w of BlueApp.wallets) { - if (w.getSecret() === ret.data) { - // lookig for duplicates - this.setState({ isLoading: false }); - if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.resumePreview(); - return alert(loc.wallets.scanQrWif.wallet_already_exists); // duplicate, not adding - } - } - - // is it HD BIP49 mnemonic? - let hd = new HDSegwitP2SHWallet(); - hd.setSecret(ret.data); - if (hd.validateMnemonic()) { - for (let w of BlueApp.wallets) { - if (w.getSecret() === hd.getSecret()) { - // lookig for duplicates - this.setState({ isLoading: false }); - if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.resumePreview(); - return alert(loc.wallets.scanQrWif.wallet_already_exists); // duplicate, not adding - } - } - this.setState({ isLoading: true }); - hd.setLabel(loc.wallets.import.imported + ' ' + hd.typeReadable); - await hd.fetchBalance(); - if (hd.getBalance() !== 0) { - await hd.fetchTransactions(); - BlueApp.wallets.push(hd); - await BlueApp.saveToDisk(); - alert(loc.wallets.import.success); - this.props.navigation.popToTop(); - setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500); - this.setState({ isLoading: false }); - return; - } - } - // nope - - // is it HD legacy (BIP44) mnemonic? - hd = new HDLegacyP2PKHWallet(); - hd.setSecret(ret.data); - if (hd.validateMnemonic()) { - for (let w of BlueApp.wallets) { - if (w.getSecret() === hd.getSecret()) { - // lookig for duplicates - this.setState({ isLoading: false }); - if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.resumePreview(); - return alert(loc.wallets.scanQrWif.wallet_already_exists); // duplicate, not adding - } - } - await hd.fetchTransactions(); - if (hd.getTransactions().length !== 0) { - await hd.fetchBalance(); - hd.setLabel(loc.wallets.import.imported + ' ' + hd.typeReadable); - BlueApp.wallets.push(hd); - await BlueApp.saveToDisk(); - alert(loc.wallets.import.success); - this.props.navigation.popToTop(); - setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500); - this.setState({ isLoading: false }); - return; - } - } - // nope - - // is it HD BIP49 mnemonic? - hd = new HDSegwitBech32Wallet(); - hd.setSecret(ret.data); - if (hd.validateMnemonic()) { - for (let w of BlueApp.wallets) { - if (w.getSecret() === hd.getSecret()) { - // lookig for duplicates - this.setState({ isLoading: false }); - if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.resumePreview(); - return alert(loc.wallets.scanQrWif.wallet_already_exists); // duplicate, not adding - } - } - this.setState({ isLoading: true }); - hd.setLabel(loc.wallets.import.imported + ' ' + hd.typeReadable); - BlueApp.wallets.push(hd); - await hd.fetchBalance(); - await hd.fetchTransactions(); - await BlueApp.saveToDisk(); - alert(loc.wallets.import.success); - this.props.navigation.popToTop(); - setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500); - this.setState({ isLoading: false }); - return; - } - // nope - - // is it lndhub? - if (ret.data.indexOf('blitzhub://') !== -1 || ret.data.indexOf('lndhub://') !== -1) { - this.setState({ isLoading: true }); - let lnd = new LightningCustodianWallet(); - lnd.setSecret(ret.data); - if (ret.data.includes('@')) { - const split = ret.data.split('@'); - lnd.setBaseURI(split[1]); - lnd.init(); - lnd.setSecret(split[0]); - } - - try { - await lnd.authorize(); - await lnd.fetchTransactions(); - await lnd.fetchUserInvoices(); - await lnd.fetchPendingTransactions(); - await lnd.fetchBalance(); - } catch (Err) { - console.log(Err); - this.setState({ isLoading: false }); - if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.resumePreview(); - alert(Err.message); - return; - } - - BlueApp.wallets.push(lnd); - lnd.setLabel(loc.wallets.import.imported + ' ' + lnd.typeReadable); - this.props.navigation.popToTop(); - alert(loc.wallets.import.success); - await BlueApp.saveToDisk(); - setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500); - this.setState({ isLoading: false }); - return; - } - // nope - - // is it just address..? - let watchOnly = new WatchOnlyWallet(); - let watchAddr = ret.data; - - // Is it BIP21 format? - if (ret.data.indexOf('bitcoin:') === 0 || ret.data.indexOf('BITCOIN:') === 0) { - try { - watchAddr = bip21.decode(ret.data).address; - } catch (err) { - console.log(err); - } - } - - if (watchOnly.setSecret(watchAddr) && watchOnly.valid()) { - watchOnly.setLabel(loc.wallets.scanQrWif.imported_watchonly); - BlueApp.wallets.push(watchOnly); - alert(loc.wallets.scanQrWif.imported_watchonly + loc.wallets.scanQrWif.with_address + watchOnly.getAddress()); - await watchOnly.fetchBalance(); - await watchOnly.fetchTransactions(); - await BlueApp.saveToDisk(); - this.props.navigation.popToTop(); - setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500); - this.setState({ isLoading: false }); - return; - } - // nope - - let newWallet = new SegwitP2SHWallet(); - newWallet.setSecret(ret.data); - let newLegacyWallet = new LegacyWallet(); - newLegacyWallet.setSecret(ret.data); - - if (newWallet.getAddress() === false && newLegacyWallet.getAddress() === false) { - alert(loc.wallets.scanQrWif.bad_wif); - if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.resumePreview(); - this.setState({ isLoading: false }); - return; - } - - if (newWallet.getAddress() === false && newLegacyWallet.getAddress() !== false) { - // case - WIF is valid, just has uncompressed pubkey - newLegacyWallet.setLabel(loc.wallets.scanQrWif.imported_legacy); - BlueApp.wallets.push(newLegacyWallet); - alert(loc.wallets.scanQrWif.imported_wif + ret.data + loc.wallets.scanQrWif.with_address + newLegacyWallet.getAddress()); - await newLegacyWallet.fetchBalance(); - await newLegacyWallet.fetchTransactions(); - await BlueApp.saveToDisk(); - this.props.navigation.popToTop(); - setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500); - return; - } - - this.setState({ isLoading: true }); - await newLegacyWallet.fetchBalance(); - console.log('newLegacyWallet == ', newLegacyWallet.getBalance()); - - if (newLegacyWallet.getBalance()) { - newLegacyWallet.setLabel(loc.wallets.scanQrWif.imported_legacy); - BlueApp.wallets.push(newLegacyWallet); - alert(loc.wallets.scanQrWif.imported_wif + ret.data + loc.wallets.scanQrWif.with_address + newLegacyWallet.getAddress()); - await newLegacyWallet.fetchTransactions(); - } else { - await newWallet.fetchBalance(); - await newWallet.fetchTransactions(); - newWallet.setLabel(loc.wallets.scanQrWif.imported_segwit); - BlueApp.wallets.push(newWallet); - alert(loc.wallets.scanQrWif.imported_wif + ret.data + loc.wallets.scanQrWif.with_address + newWallet.getAddress()); - } - await BlueApp.saveToDisk(); - this.props.navigation.popToTop(); - setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500); - }; // end - - render() { - if (this.state.isLoading) { - return ( - - - - ); - } - return ( - - {(() => { - if (this.state.message) { - return ( - - - {this.state.message} - { - this.setState({ message: false }); - shold_stop_bip38 = true; // eslint-disable-line - }} - title={loc.wallets.scanQrWif.cancel} - /> - - - ); - } else { - return ( - - (this.cameraRef = ref)} - barCodeTypes={[RNCamera.Constants.BarCodeType.qr]} - /> - this.props.navigation.goBack(null)} - > - - - - ); - } - })()} - - ); - } -} - -ScanQrWif.propTypes = { - navigation: PropTypes.shape({ - goBack: PropTypes.func, - popToTop: PropTypes.func, - navigate: PropTypes.func, - }), -};