Browse Source

REF: Reworked Import wallet flow

visualfix
Marcos Rodriguez 5 years ago
committed by Overtorment
parent
commit
c975347b6f
  1. 75
      BlueComponents.js
  2. 4
      MainBottomTabs.js
  3. 13
      class/app-storage.js
  4. 1
      class/index.js
  5. 31
      class/placeholder-wallet.js
  6. 4
      class/walletGradient.js
  7. 228
      class/walletImport.js
  8. 307
      screen/wallets/import.js
  9. 52
      screen/wallets/list.js
  10. 4
      screen/wallets/reorderWallets.js
  11. 344
      screen/wallets/scanQrWif.js

75
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,6 +748,7 @@ export class BlueHeaderDefaultMain extends Component {
</Text>
}
rightComponent={
this.props.onNewWalletPress && (
<TouchableOpacity
onPress={this.props.onNewWalletPress}
style={{
@ -757,6 +758,7 @@ export class BlueHeaderDefaultMain extends Component {
>
<BluePlusIcon />
</TouchableOpacity>
)
}
/>
</SafeAreaView>
@ -957,7 +959,7 @@ export class BlueLoading extends Component {
render() {
return (
<SafeBlueArea>
<View style={{ flex: 1, paddingTop: 200 }}>
<View style={{ flex: 1, paddingTop: 200 }} {...this.props}>
<ActivityIndicator />
</View>
</SafeBlueArea>
@ -1844,6 +1846,74 @@ export class WalletsCarousel extends Component {
);
}
if (item.type === PlaceholderWallet.type) {
return (
<Animated.View
style={{ paddingRight: 10, marginVertical: 17, transform: [{ scale: scaleValue }] }}
shadowOpacity={40 / 100}
shadowOffset={{ width: 0, height: 0 }}
shadowRadius={5}
>
<TouchableWithoutFeedback
onPressIn={item.getIsFailure() ? this.onPressedIn : null}
onPressOut={item.getIsFailure() ? this.onPressedOut : null}
onPress={() => {
if (item.getIsFailure() && WalletsCarousel.handleClick) {
WalletsCarousel.handleClick(index);
}
}}
>
<LinearGradient
shadowColor={BlueApp.settings.shadowColor}
colors={WalletGradient.gradientsFor(item.type)}
style={{
padding: 15,
borderRadius: 10,
minHeight: 164,
elevation: 5,
}}
>
<Image
source={require('./img/btc-shape.png')}
style={{
width: 99,
height: 94,
position: 'absolute',
bottom: 0,
right: 0,
}}
/>
<Text style={{ backgroundColor: 'transparent' }} />
<Text
numberOfLines={1}
style={{
backgroundColor: 'transparent',
fontSize: 19,
color: BlueApp.settings.inverseForegroundColor,
}}
>
{item.getLabel()}
</Text>
{item.getIsFailure() ? (
<Text
numberOfLines={0}
style={{
backgroundColor: 'transparent',
fontSize: 19,
marginTop: 40,
color: BlueApp.settings.inverseForegroundColor,
}}
>
An error was encountered when attempting to import this wallet.
</Text>
) : (
<ActivityIndicator style={{ marginTop: 40 }} />
)}
</LinearGradient>
</TouchableWithoutFeedback>
</Animated.View>
);
} else {
return (
<Animated.View
style={{ paddingRight: 10, marginVertical: 17, transform: [{ scale: scaleValue }] }}
@ -1936,6 +2006,7 @@ export class WalletsCarousel extends Component {
</Animated.View>
);
}
}
snapToItem = item => {
this.walletsCarousel.current.snapToItem(item);

4
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,
},

13
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) {

1
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';

31
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;
}
}

4
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;

228
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);
}
}

307
screen/wallets/import.js

@ -1,253 +1,54 @@
/* 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,
};
constructor(props) {
super(props);
this.state = {
isLoading: true,
isToolbarVisibleForAndroid: false,
};
}
const WalletsImport = () => {
const [isToolbarVisibleForAndroid, setIsToolbarVisibleForAndroid] = useState(false);
const [importText, setImportText] = useState(useNavigationParam('label') || '');
const { navigate, dismiss } = useNavigation();
componentDidMount() {
this.setState({
isLoading: false,
label: '',
});
useEffect(() => {
Privacy.enableBlur();
}
componentWillUnmount() {
Privacy.disableBlur();
}
return () => 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();
}
const importButtonPressed = () => {
if (importText.trim().length === 0) {
return;
}
importMnemonic(importText);
};
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 });
// 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
}
setLabel(text) {
this.setState({
label: text,
}); /* also, a hack to make screen update new typed text */
}
};
render() {
if (this.state.isLoading) {
return (
<View style={{ flex: 1, paddingTop: 20 }}>
<BlueLoading />
</View>
);
}
const onBarScanned = value => {
setImportText(value);
importMnemonic(value);
};
return (
<SafeBlueArea forceInset={{ horizontal: 'always' }} style={{ flex: 1, paddingTop: 40 }}>
@ -256,27 +57,36 @@ export default class WalletsImport extends Component {
<BlueFormLabel>{loc.wallets.import.explanation}</BlueFormLabel>
<BlueSpacing20 />
<BlueFormMultiInput
value={this.state.label}
placeholder=""
value={importText}
contextMenuHidden
onChangeText={text => {
this.setLabel(text);
}}
onChangeText={setImportText}
inputAccessoryViewID={BlueDoneAndDismissKeyboardInputAccessory.InputAccessoryViewID}
onFocus={() => this.setState({ isToolbarVisibleForAndroid: true })}
onBlur={() => this.setState({ isToolbarVisibleForAndroid: false })}
onFocus={() => setIsToolbarVisibleForAndroid(true)}
onBlur={() => setIsToolbarVisibleForAndroid(false)}
/>
{Platform.select({
ios: (
<BlueDoneAndDismissKeyboardInputAccessory
onClearTapped={() => this.setState({ label: '' }, () => Keyboard.dismiss())}
onPasteTapped={text => this.setState({ label: text }, () => Keyboard.dismiss())}
onClearTapped={() => {
setImportText('');
Keyboard.dismiss();
}}
onPasteTapped={text => {
setImportText(text);
Keyboard.dismiss();
}}
/>
),
android: this.state.isToolbarVisibleForAndroid && (
android: isToolbarVisibleForAndroid && (
<BlueDoneAndDismissKeyboardInputAccessory
onClearTapped={() => this.setState({ label: '' }, () => Keyboard.dismiss())}
onPasteTapped={text => this.setState({ label: text }, () => Keyboard.dismiss())}
onClearTapped={() => {
setImportText('');
Keyboard.dismiss();
}}
onPasteTapped={text => {
setImportText(text);
Keyboard.dismiss();
}}
/>
),
})}
@ -290,37 +100,26 @@ export default class WalletsImport extends Component {
}}
>
<BlueButton
disabled={!this.state.label}
disabled={importText.trim().length === 0}
title={loc.wallets.import.do_import}
buttonStyle={{
width: width / 1.5,
}}
onPress={async () => {
if (!this.state.label) {
return;
}
this.setState({ isLoading: true }, async () => {
await this.importMnemonic(this.state.label.trim());
this.setState({ isLoading: false });
});
}}
onPress={importButtonPressed}
/>
<BlueButtonLink
title={loc.wallets.import.scan_qr}
onPress={() => {
this.props.navigation.navigate('ScanQrWif');
navigate('ScanQrAddress', { onBarScanned });
}}
/>
</View>
</SafeBlueArea>
);
}
}
};
WalletsImport.propTypes = {
navigation: PropTypes.shape({
navigate: PropTypes.func,
goBack: PropTypes.func,
dismiss: PropTypes.func,
}),
WalletsImport.navigationOptions = {
...BlueNavigationStyle(),
title: loc.wallets.import.title,
};
export default WalletsImport;

52
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,15 +154,44 @@ export default class WalletsList extends Component {
console.log('click', index);
let wallet = BlueApp.wallets[index];
if (wallet) {
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
if (!BlueApp.getWallets().some(wallet => wallet.type === PlaceholderWallet.type)) {
this.props.navigation.navigate('AddWallet');
}
}
}
onSnapToItem(index) {
console.log('onSnapToItem', index);
@ -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 {
<RefreshControl onRefresh={() => this.refreshTransactions()} refreshing={!this.state.isFlatListRefreshControlHidden} />
}
>
<BlueHeaderDefaultMain leftText={loc.wallets.list.title} onNewWalletPress={() => this.props.navigation.navigate('AddWallet')} />
<BlueHeaderDefaultMain
leftText={loc.wallets.list.title}
onNewWalletPress={
!BlueApp.getWallets().some(wallet => wallet.type === PlaceholderWallet.type)
? () => this.props.navigation.navigate('AddWallet')
: null
}
/>
<WalletsCarousel
removeClippedSubviews={false}
data={this.state.wallets}

4
screen/wallets/reorderWallets.js

@ -4,7 +4,7 @@ import { SafeBlueArea, BlueNavigationStyle } from '../../BlueComponents';
import SortableList from 'react-native-sortable-list';
import LinearGradient from 'react-native-linear-gradient';
import PropTypes from 'prop-types';
import { LightningCustodianWallet } from '../../class/lightning-custodian-wallet';
import { PlaceholderWallet, LightningCustodianWallet } from '../../class';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import WalletGradient from '../../class/walletGradient';
let EV = require('../../events');
@ -51,7 +51,7 @@ export default class ReorderWallets extends Component {
},
});
const wallets = BlueApp.getWallets();
const wallets = BlueApp.getWallets().filter(wallet => wallet.type !== PlaceholderWallet.type);
this.setState({
data: wallets,
isLoading: false,

344
screen/wallets/scanQrWif.js

@ -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 (
<View style={{ flex: 1, paddingTop: 20, justifyContent: 'center', alignContent: 'center' }}>
<ActivityIndicator />
</View>
);
}
return (
<View style={{ flex: 1 }}>
{(() => {
if (this.state.message) {
return (
<SafeBlueArea>
<View
style={{
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
}}
>
<BlueText>{this.state.message}</BlueText>
<BlueButton
icon={{ name: 'ban', type: 'font-awesome' }}
onPress={async () => {
this.setState({ message: false });
shold_stop_bip38 = true; // eslint-disable-line
}}
title={loc.wallets.scanQrWif.cancel}
/>
</View>
</SafeBlueArea>
);
} else {
return (
<SafeBlueArea style={{ flex: 1 }}>
<RNCamera
captureAudio={false}
androidCameraPermissionOptions={{
title: 'Permission to use camera',
message: 'We need your permission to use your camera',
buttonPositive: 'OK',
buttonNegative: 'Cancel',
}}
style={{ flex: 1, justifyContent: 'space-between' }}
onBarCodeRead={this.onBarCodeScanned}
ref={ref => (this.cameraRef = ref)}
barCodeTypes={[RNCamera.Constants.BarCodeType.qr]}
/>
<TouchableOpacity
style={{
width: 40,
height: 40,
marginLeft: 24,
backgroundColor: '#FFFFFF',
justifyContent: 'center',
borderRadius: 20,
position: 'absolute',
top: 64,
}}
onPress={() => this.props.navigation.goBack(null)}
>
<Image style={{ alignSelf: 'center' }} source={require('../../img/close.png')} />
</TouchableOpacity>
</SafeBlueArea>
);
}
})()}
</View>
);
}
}
ScanQrWif.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.func,
popToTop: PropTypes.func,
navigate: PropTypes.func,
}),
};
Loading…
Cancel
Save