From cd7526dc330374559059ebc297101dc4fde4ad33 Mon Sep 17 00:00:00 2001 From: Overtorment Date: Thu, 26 Dec 2019 22:14:29 +0000 Subject: [PATCH 01/26] OPS: fix appcenter android build --- appcenter-post-build.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/appcenter-post-build.sh b/appcenter-post-build.sh index e32b7cb4..2d537127 100755 --- a/appcenter-post-build.sh +++ b/appcenter-post-build.sh @@ -22,6 +22,7 @@ if [ -f $FILENAME ]; then FILENAME_UNIQ="$APPCENTER_OUTPUT_DIRECTORY/$HASH.apk" cp "$FILENAME" "$FILENAME_UNIQ" curl "http://filestorage.bluewallet.io:1488/upload.php" -F "fileToUpload=@$FILENAME_UNIQ" + rm "$FILENAME_UNIQ" DLOAD_APK="http://filestorage.bluewallet.io:1488/$HASH.apk" curl -X POST --data "{\"body\":\"♫ This was a triumph. I'm making a note here: HUGE SUCCESS ♫\n\n [android in browser] $APPURL\n\n[download apk]($DLOAD_APK) \"}" -u "$GITHUB" "https://api.github.com/repos/BlueWallet/BlueWallet/issues/$PR/comments" From c2eb13dd30ad3818835c4d6db573eecceb1f2e7a Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Date: Mon, 23 Dec 2019 23:31:54 -0600 Subject: [PATCH 02/26] ADD: Show LNDHub backup when creating lnd wallet --- MainBottomTabs.js | 9 +++++ screen/wallets/add.js | 32 +++-------------- screen/wallets/pleaseBackupLNDHub.js | 51 ++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 28 deletions(-) create mode 100644 screen/wallets/pleaseBackupLNDHub.js diff --git a/MainBottomTabs.js b/MainBottomTabs.js index 9e4af6d1..03ffee14 100644 --- a/MainBottomTabs.js +++ b/MainBottomTabs.js @@ -16,6 +16,7 @@ import WalletsList from './screen/wallets/list'; import WalletTransactions from './screen/wallets/transactions'; import AddWallet from './screen/wallets/add'; import PleaseBackup from './screen/wallets/pleaseBackup'; +import PleaseBackupLNDHub from './screen/wallets/pleaseBackupLNDHub'; import ImportWallet from './screen/wallets/import'; import WalletDetails from './screen/wallets/details'; import WalletExport from './screen/wallets/export'; @@ -210,6 +211,14 @@ const CreateWalletStackNavigator = createStackNavigator({ PleaseBackup: { screen: PleaseBackup, }, + PleaseBackupLNDHub: { + screen: PleaseBackupLNDHub, + swipeEnabled: false, + gesturesEnabled: false, + navigationOptions: { + header: null, + }, + }, }); const LightningScanInvoiceStackNavigator = createStackNavigator({ diff --git a/screen/wallets/add.js b/screen/wallets/add.js index 11ed0934..e1c3d4b1 100644 --- a/screen/wallets/add.js +++ b/screen/wallets/add.js @@ -1,7 +1,6 @@ /* global alert */ import React, { Component } from 'react'; import { - Alert, Text, ScrollView, LayoutAnimation, @@ -269,34 +268,11 @@ export default class WalletsAdd extends Component { EV(EV.enum.WALLETS_COUNT_CHANGED); A(A.ENUM.CREATED_WALLET); ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false }); - this.props.navigation.dismiss(); + this.props.navigation.navigate('PleaseBackupLNDHub', { + wallet: w, + }); }; - - if (!BlueApp.getWallets().some(wallet => wallet.type !== LightningCustodianWallet.type)) { - Alert.alert( - loc.wallets.add.lightning, - loc.wallets.createBitcoinWallet, - [ - { - text: loc.send.details.cancel, - style: 'cancel', - onPress: () => { - this.setState({ isLoading: false }); - }, - }, - { - text: loc._.ok, - style: 'default', - onPress: () => { - this.createLightningWallet(); - }, - }, - ], - { cancelable: false }, - ); - } else { - this.createLightningWallet(); - } + this.createLightningWallet(); } else if (this.state.selectedIndex === 2) { // zero index radio - HD segwit w = new HDSegwitP2SHWallet(); diff --git a/screen/wallets/pleaseBackupLNDHub.js b/screen/wallets/pleaseBackupLNDHub.js new file mode 100644 index 00000000..9c260fbb --- /dev/null +++ b/screen/wallets/pleaseBackupLNDHub.js @@ -0,0 +1,51 @@ +import React, { useState } from 'react'; +import { useNavigation, useNavigationParam } from 'react-navigation-hooks'; +import { View, Dimensions } from 'react-native'; +import { SafeBlueArea, BlueSpacing20, BlueCopyTextToClipboard, BlueButton, BlueCard, BlueTextCentered } from '../../BlueComponents'; +import QRCode from 'react-native-qrcode-svg'; +import { ScrollView } from 'react-native-gesture-handler'; +const { height, width } = Dimensions.get('window'); +const BlueApp = require('../../BlueApp'); + +const PleaseBackupLNDHub = () => { + const wallet = useNavigationParam('wallet'); + const navigation = useNavigation(); + const [qrCodeHeight, setQrCodeHeight] = useState(height > width ? width - 40 : width / 2); + + const onLayout = () => { + const { height } = Dimensions.get('window'); + setQrCodeHeight(height > width ? width - 40 : width / 2); + }; + + return ( + + + + + + Please take a moment to save this LNDHub authentication. It's your backup you can use to restore the wallet on other device. + + + + + + + + + + + + + + ); +}; + +export default PleaseBackupLNDHub; From 29d35c36b5c658eddbdecc5c41e7e442dfa3d353 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Date: Tue, 24 Dec 2019 09:40:15 -0600 Subject: [PATCH 03/26] ADD: Export screen allows copying to clipboard if its a LNDHub wallet --- screen/wallets/export.js | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/screen/wallets/export.js b/screen/wallets/export.js index 5a2cd3d7..9c00240e 100644 --- a/screen/wallets/export.js +++ b/screen/wallets/export.js @@ -1,10 +1,11 @@ import React, { Component } from 'react'; -import { Dimensions, ActivityIndicator, View } from 'react-native'; +import { Dimensions, ScrollView, ActivityIndicator, View } from 'react-native'; import QRCode from 'react-native-qrcode-svg'; -import { BlueSpacing20, SafeBlueArea, BlueNavigationStyle, BlueText } from '../../BlueComponents'; +import { BlueSpacing20, SafeBlueArea, BlueNavigationStyle, BlueText, BlueCopyTextToClipboard, BlueCard } from '../../BlueComponents'; import PropTypes from 'prop-types'; import Privacy from '../../Privacy'; import Biometric from '../../class/biometrics'; +import { LightningCustodianWallet } from '../../class'; /** @type {AppStorage} */ let BlueApp = require('../../BlueApp'); let loc = require('../../loc'); @@ -72,7 +73,11 @@ export default class WalletExport extends Component { return ( - + {this.state.wallet.typeReadable} @@ -80,9 +85,9 @@ export default class WalletExport extends Component { {(() => { if (this.state.wallet.getAddress()) { return ( - + {this.state.wallet.getAddress()} - + ); } })()} @@ -99,9 +104,12 @@ export default class WalletExport extends Component { /> - - {this.state.wallet.getSecret()} - + {this.state.wallet.type === LightningCustodianWallet.type ? ( + + ) : ( + {this.state.wallet.getSecret()} + )} + ); } From ec5bc4a3c6f58be20e59b8fd8942c6a28b6dd8c7 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Date: Tue, 24 Dec 2019 21:35:47 -0600 Subject: [PATCH 04/26] ADD: Ask user if they have backed up their seed phrase --- BlueComponents.js | 16 +++++ class/abstract-wallet.js | 9 +++ loc/es.js | 2 +- screen/lnd/lndCreateInvoice.js | 33 +++++++++- screen/receive/details.js | 115 +++++++++++++++++++-------------- screen/wallets/export.js | 12 +++- screen/wallets/import.js | 1 + screen/wallets/transactions.js | 20 +++++- 8 files changed, 150 insertions(+), 58 deletions(-) diff --git a/BlueComponents.js b/BlueComponents.js index 1f9a1552..2a5d577a 100644 --- a/BlueComponents.js +++ b/BlueComponents.js @@ -7,6 +7,7 @@ import { TouchableOpacity, TouchableWithoutFeedback, Animated, + Alert, ActivityIndicator, View, KeyboardAvoidingView, @@ -375,6 +376,21 @@ export class BlueButtonLink extends Component { } } +export const BlueAlertWalletExportReminder = ({ onSuccess = () => {}, onFailure }) => { + Alert.alert( + 'Wallet', + `Have your saved your wallet's backup phrase? This backup phrase is required to access your funds in case you lose this device. Without the backup phrase, your funds will be permanently lost.`, + [ + { text: 'Yes, I have', onPress: onSuccess, style: 'cancel' }, + { + text: 'No, I have not', + onPress: onFailure, + }, + ], + { cancelable: false }, + ); +}; + export const BlueNavigationStyle = (navigation, withNavigationCloseButton = false, customCloseButtonFunction = undefined) => ({ headerStyle: { backgroundColor: BlueApp.settings.brandingColor, diff --git a/class/abstract-wallet.js b/class/abstract-wallet.js index eea6540d..bf0eea6e 100644 --- a/class/abstract-wallet.js +++ b/class/abstract-wallet.js @@ -29,6 +29,7 @@ export class AbstractWallet { this.preferredBalanceUnit = BitcoinUnit.BTC; this.chain = Chain.ONCHAIN; this.hideBalance = false; + this.userHasSavedExport = false; } getID() { @@ -42,6 +43,14 @@ export class AbstractWallet { return this.transactions; } + getUserHasSavedExport() { + return this.userHasSavedExport; + } + + setUserHasSavedExport(value) { + this.userHasSavedExport = value; + } + /** * * @returns {string} diff --git a/loc/es.js b/loc/es.js index edaa2bde..89eaf0f3 100644 --- a/loc/es.js +++ b/loc/es.js @@ -18,7 +18,7 @@ module.exports = { header: 'Un Monedero esta representado con secreto (clave privada) y una dirección' + 'que puedes compartir para recibir monedas.', add: 'Añadir Carterqa', create_a_wallet: 'Crear una billetera', - create_a_wallet1: 'Es gratis y puedes crear cuantas deseas', + create_a_wallet1: 'Es gratis y puedes crear', create_a_wallet2: 'cuantas usted quiera', latest_transaction: 'última transacción', empty_txs1: 'Sus transacciones aparecerán aquí,', diff --git a/screen/lnd/lndCreateInvoice.js b/screen/lnd/lndCreateInvoice.js index 65415be1..1ce9ee5e 100644 --- a/screen/lnd/lndCreateInvoice.js +++ b/screen/lnd/lndCreateInvoice.js @@ -10,7 +10,13 @@ import { TouchableOpacity, Text, } from 'react-native'; -import { BlueNavigationStyle, BlueButton, BlueBitcoinAmount, BlueDismissKeyboardInputAccessory } from '../../BlueComponents'; +import { + BlueNavigationStyle, + BlueButton, + BlueBitcoinAmount, + BlueDismissKeyboardInputAccessory, + BlueAlertWalletExportReminder, +} from '../../BlueComponents'; import { LightningCustodianWallet } from '../../class/lightning-custodian-wallet'; import PropTypes from 'prop-types'; import bech32 from 'bech32'; @@ -47,16 +53,36 @@ export default class LNDCreateInvoice extends Component { fromWallet, amount: '', description: '', - isLoading: false, lnurl: '', lnurlParams: null, + isLoading: true, }; } - componentDidMount() { + renderReceiveDetails = async () => { + this.state.fromWallet.setUserHasSavedExport(true); + await BlueApp.saveToDisk(); if (this.props.navigation.state.params.uri) { this.processLnurl(this.props.navigation.getParam('uri')); } + this.setState({ isLoading: false }); + }; + + componentDidMount() { + if (this.state.fromWallet.getUserHasSavedExport()) { + this.renderReceiveDetails(); + } else { + BlueAlertWalletExportReminder({ + onSuccess: this.renderReceiveDetails, + onFailure: () => { + this.props.navigation.dismiss(); + this.props.navigation.navigate('WalletExport', { + address: this.state.fromWallet.getAddress(), + secret: this.state.fromWallet.getSecret(), + }); + }, + }); + } } async createInvoice() { @@ -266,6 +292,7 @@ export default class LNDCreateInvoice extends Component { LNDCreateInvoice.propTypes = { navigation: PropTypes.shape({ goBack: PropTypes.func, + dismiss: PropTypes.func, navigate: PropTypes.func, getParam: PropTypes.func, state: PropTypes.shape({ diff --git a/screen/receive/details.js b/screen/receive/details.js index d5f58496..68a67158 100644 --- a/screen/receive/details.js +++ b/screen/receive/details.js @@ -13,6 +13,7 @@ import { BlueBitcoinAmount, BlueText, BlueSpacing20, + BlueAlertWalletExportReminder, } from '../../BlueComponents'; import PropTypes from 'prop-types'; import Privacy from '../../Privacy'; @@ -45,64 +46,78 @@ export default class ReceiveDetails extends Component { }; } - async componentDidMount() { - Privacy.enableBlur(); - console.log('receive/details - componentDidMount'); - - { - let address; - let wallet; - for (let w of BlueApp.getWallets()) { - if (w.getSecret() === this.state.secret) { - // found our wallet - wallet = w; + renderReceiveDetails = async () => { + this.wallet.setUserHasSavedExport(true); + await BlueApp.saveToDisk(); + let address; + if (this.wallet.getAddressAsync) { + if (this.wallet.chain === Chain.ONCHAIN) { + try { + address = await Promise.race([this.wallet.getAddressAsync(), BlueApp.sleep(1000)]); + } catch (_) {} + if (!address) { + // either sleep expired or getAddressAsync threw an exception + console.warn('either sleep expired or getAddressAsync threw an exception'); + address = this.wallet._getExternalAddressByIndex(this.wallet.next_free_address_index); + } else { + BlueApp.saveToDisk(); // caching whatever getAddressAsync() generated internally } - } - if (wallet) { - if (wallet.getAddressAsync) { - if (wallet.chain === Chain.ONCHAIN) { - try { - address = await Promise.race([wallet.getAddressAsync(), BlueApp.sleep(1000)]); - } catch (_) {} - if (!address) { - // either sleep expired or getAddressAsync threw an exception - console.warn('either sleep expired or getAddressAsync threw an exception'); - address = wallet._getExternalAddressByIndex(wallet.next_free_address_index); - } else { - BlueApp.saveToDisk(); // caching whatever getAddressAsync() generated internally - } - this.setState({ - address: address, - }); - } else if (wallet.chain === Chain.OFFCHAIN) { - try { - await Promise.race([wallet.getAddressAsync(), BlueApp.sleep(1000)]); - address = wallet.getAddress(); - } catch (_) {} - if (!address) { - // either sleep expired or getAddressAsync threw an exception - console.warn('either sleep expired or getAddressAsync threw an exception'); - address = wallet.getAddress(); - } else { - BlueApp.saveToDisk(); // caching whatever getAddressAsync() generated internally - } - } - console.warn(address) - this.setState({ - address: address, - }); - } else if (wallet.getAddress) { - this.setState({ - address: wallet.getAddress(), - }); + this.setState({ + address: address, + }); + } else if (this.wallet.chain === Chain.OFFCHAIN) { + try { + await Promise.race([this.wallet.getAddressAsync(), BlueApp.sleep(1000)]); + address = this.wallet.getAddress(); + } catch (_) {} + if (!address) { + // either sleep expired or getAddressAsync threw an exception + console.warn('either sleep expired or getAddressAsync threw an exception'); + address = this.wallet.getAddress(); + } else { + BlueApp.saveToDisk(); // caching whatever getAddressAsync() generated internally } } + this.setState({ + address: address, + }); + } else if (this.wallet.getAddress) { + this.setState({ + address: this.wallet.getAddress(), + }); } - InteractionManager.runAfterInteractions(async () => { const bip21encoded = bip21.encode(this.state.address); this.setState({ bip21encoded }); }); + }; + + componentDidMount() { + Privacy.enableBlur(); + console.log('receive/details - componentDidMount'); + + for (let w of BlueApp.getWallets()) { + if (w.getSecret() === this.state.secret) { + // found our wallet + this.wallet = w; + } + } + if (this.wallet) { + if (!this.wallet.getUserHasSavedExport()) { + BlueAlertWalletExportReminder({ + onSuccess: this.renderReceiveDetails, + onFailure: () => { + this.props.navigation.goBack(); + this.props.navigation.navigate('WalletExport', { + address: this.wallet.getAddress(), + secret: this.wallet.getSecret(), + }); + }, + }); + } else { + this.renderReceiveDetails(); + } + } } componentWillUnmount() { diff --git a/screen/wallets/export.js b/screen/wallets/export.js index 9c00240e..e2655f41 100644 --- a/screen/wallets/export.js +++ b/screen/wallets/export.js @@ -48,9 +48,15 @@ export default class WalletExport extends Component { } } - this.setState({ - isLoading: false, - }); + this.setState( + { + isLoading: false, + }, + () => { + this.state.wallet.setUserHasSavedExport(true); + BlueApp.saveToDisk(); + }, + ); } async componentWillUnmount() { diff --git a/screen/wallets/import.js b/screen/wallets/import.js index e952200a..ab590a75 100644 --- a/screen/wallets/import.js +++ b/screen/wallets/import.js @@ -65,6 +65,7 @@ export default class WalletsImport extends Component { 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); diff --git a/screen/wallets/transactions.js b/screen/wallets/transactions.js index 0a279498..b1ef7ba0 100644 --- a/screen/wallets/transactions.js +++ b/screen/wallets/transactions.js @@ -25,6 +25,7 @@ import { BlueReceiveButtonIcon, BlueTransactionListItem, BlueWalletNavigationHeader, + BlueAlertWalletExportReminder, } from '../../BlueComponents'; import WalletGradient from '../../class/walletGradient'; import { Icon } from 'react-native-elements'; @@ -433,7 +434,24 @@ export default class WalletTransactions extends Component { this.setState({ wallet }, () => InteractionManager.runAfterInteractions(() => BlueApp.saveToDisk())); }) } - onManageFundsPressed={() => this.setState({ isManageFundsModalVisible: true })} + onManageFundsPressed={() => { + if (this.state.wallet.getUserHasSavedExport()) { + this.setState({ isManageFundsModalVisible: true }); + } else { + BlueAlertWalletExportReminder({ + onSuccess: async () => { + this.state.wallet.setUserHasSavedExport(true); + await BlueApp.saveToDisk(); + this.setState({ isManageFundsModalVisible: true }); + }, + onFailure: () => + this.props.navigation.navigate('WalletExport', { + address: this.state.wallet.getAddress(), + secret: this.state.wallet.getSecret(), + }), + }); + } + }} /> Date: Thu, 26 Dec 2019 20:21:07 -0600 Subject: [PATCH 05/26] 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 | 393 ++++++++----------------------- screen/wallets/list.js | 62 ++++- screen/wallets/reorderWallets.js | 4 +- screen/wallets/scanQrWif.js | 344 --------------------------- 11 files changed, 578 insertions(+), 747 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 2a5d577a..5355b6c4 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()} - )} - - - {loc.wallets.list.latest_transaction} - - + An error was encountered when attempting to import this wallet. + + ) : ( + + )} + + + + ); + } else { + return ( + + { + if (WalletsCarousel.handleClick) { + WalletsCarousel.handleClick(index); + } + }} + > + - {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 03ffee14..aadd42e6 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 bdba64a1..df9f9aac 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 c6add4c0..1ec5ece2 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 00000000..206b3b59 --- /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 46c4d2ef..a861a4ef 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 00000000..969506d5 --- /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 ab590a75..86ecbe56 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, - }; - - 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(); - } + return () => Privacy.disableBlur(); + }); - 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(); + 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); - } - - 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 ( - - - - ); + WalletImport.processImportText(importText); + dismiss(); + } catch (error) { + alert(loc.wallets.import.error); + ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); } + }; - 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())} - /> - ), - })} - - + const onBarScanned = value => { + setImportText(value); + importMnemonic(value); + }; - - - { - if (!this.state.label) { - return; - } - this.setState({ isLoading: true }, async () => { - await this.importMnemonic(this.state.label.trim()); - this.setState({ isLoading: false }); - }); - }} + return ( + + + + {loc.wallets.import.explanation} + + setIsToolbarVisibleForAndroid(true)} + onBlur={() => setIsToolbarVisibleForAndroid(false)} /> - { - this.props.navigation.navigate('ScanQrWif'); - }} - /> - - - ); - } -} + {Platform.select({ + ios: ( + { + setImportText(''); + Keyboard.dismiss(); + }} + onPasteTapped={text => { + setImportText(text); + Keyboard.dismiss(); + }} + /> + ), + android: isToolbarVisibleForAndroid && ( + { + setImportText(''); + Keyboard.dismiss(); + }} + onPasteTapped={text => { + setImportText(text); + Keyboard.dismiss(); + }} + /> + ), + })} + + + + + + + { + navigate('ScanQrAddress', { onBarScanned }); + }} + /> + + + ); +}; -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; diff --git a/screen/wallets/list.js b/screen/wallets/list.js index 43c506b5..2c400cfd 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 a295d208..00000000 --- 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, - }), -}; From 51e0d7d36c480c08b96dc14011425d2181cfbc71 Mon Sep 17 00:00:00 2001 From: Overtorment Date: Mon, 23 Dec 2019 23:11:00 +0000 Subject: [PATCH 06/26] REF: BIP49 to use electrum --- class/abstract-hd-electrum-wallet.js | 891 +++++++++++++++++++++++++++ class/hd-segwit-bech32-wallet.js | 885 +------------------------- class/hd-segwit-p2sh-wallet.js | 201 +----- tests/integration/HDWallet.test.js | 36 +- 4 files changed, 941 insertions(+), 1072 deletions(-) create mode 100644 class/abstract-hd-electrum-wallet.js diff --git a/class/abstract-hd-electrum-wallet.js b/class/abstract-hd-electrum-wallet.js new file mode 100644 index 00000000..7e1bca83 --- /dev/null +++ b/class/abstract-hd-electrum-wallet.js @@ -0,0 +1,891 @@ +import { NativeModules } from 'react-native'; +import bip39 from 'bip39'; +import BigNumber from 'bignumber.js'; +import b58 from 'bs58check'; +import { AbstractHDWallet } from './abstract-hd-wallet'; +const bitcoin = require('bitcoinjs-lib'); +const BlueElectrum = require('../BlueElectrum'); +const HDNode = require('bip32'); +const coinSelectAccumulative = require('coinselect/accumulative'); +const coinSelectSplit = require('coinselect/split'); + +const { RNRandomBytes } = NativeModules; + +export class AbstractHDElectrumWallet extends AbstractHDWallet { + static type = 'abstract'; + static typeReadable = 'abstract'; + static defaultRBFSequence = 2147483648; // 1 << 31, minimum for replaceable transactions as per BIP68 + static finalRBFSequence = 4294967295; // 0xFFFFFFFF + + constructor() { + super(); + this._balances_by_external_index = {}; // 0 => { c: 0, u: 0 } // confirmed/unconfirmed + this._balances_by_internal_index = {}; + + this._txs_by_external_index = {}; + this._txs_by_internal_index = {}; + + this._utxo = []; + } + + /** + * @inheritDoc + */ + getBalance() { + let ret = 0; + for (let bal of Object.values(this._balances_by_external_index)) { + ret += bal.c; + } + for (let bal of Object.values(this._balances_by_internal_index)) { + ret += bal.c; + } + return ret + (this.getUnconfirmedBalance() < 0 ? this.getUnconfirmedBalance() : 0); + } + + /** + * @inheritDoc + */ + timeToRefreshTransaction() { + for (let tx of this.getTransactions()) { + if (tx.confirmations < 7) return true; + } + return false; + } + + getUnconfirmedBalance() { + let ret = 0; + for (let bal of Object.values(this._balances_by_external_index)) { + ret += bal.u; + } + for (let bal of Object.values(this._balances_by_internal_index)) { + ret += bal.u; + } + return ret; + } + + async generate() { + let that = this; + return new Promise(function(resolve) { + if (typeof RNRandomBytes === 'undefined') { + // CLI/CI environment + // crypto should be provided globally by test launcher + return crypto.randomBytes(32, (err, buf) => { // eslint-disable-line + if (err) throw err; + that.secret = bip39.entropyToMnemonic(buf.toString('hex')); + resolve(); + }); + } + + // RN environment + RNRandomBytes.randomBytes(32, (err, bytes) => { + if (err) throw new Error(err); + let b = Buffer.from(bytes, 'base64').toString('hex'); + that.secret = bip39.entropyToMnemonic(b); + resolve(); + }); + }); + } + + _getExternalWIFByIndex(index) { + return this._getWIFByIndex(false, index); + } + + _getInternalWIFByIndex(index) { + return this._getWIFByIndex(true, index); + } + + /** + * Get internal/external WIF by wallet index + * @param {Boolean} internal + * @param {Number} index + * @returns {string|false} Either string WIF or FALSE if error happened + * @private + */ + _getWIFByIndex(internal, index) { + if (!this.secret) return false; + const mnemonic = this.secret; + const seed = bip39.mnemonicToSeed(mnemonic); + const root = HDNode.fromSeed(seed); + const path = `m/84'/0'/0'/${internal ? 1 : 0}/${index}`; + const child = root.derivePath(path); + + return child.toWIF(); + } + + _getNodeAddressByIndex(node, index) { + index = index * 1; // cast to int + if (node === 0) { + if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit + } + + if (node === 1) { + if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit + } + + if (node === 0 && !this._node0) { + const xpub = this.constructor._zpubToXpub(this.getXpub()); + const hdNode = HDNode.fromBase58(xpub); + this._node0 = hdNode.derive(node); + } + + if (node === 1 && !this._node1) { + const xpub = this.constructor._zpubToXpub(this.getXpub()); + const hdNode = HDNode.fromBase58(xpub); + this._node1 = hdNode.derive(node); + } + + let address; + if (node === 0) { + address = this.constructor._nodeToBech32SegwitAddress(this._node0.derive(index)); + } + + if (node === 1) { + address = this.constructor._nodeToBech32SegwitAddress(this._node1.derive(index)); + } + + if (node === 0) { + return (this.external_addresses_cache[index] = address); + } + + if (node === 1) { + return (this.internal_addresses_cache[index] = address); + } + } + + _getNodePubkeyByIndex(node, index) { + index = index * 1; // cast to int + + if (node === 0 && !this._node0) { + const xpub = this.constructor._zpubToXpub(this.getXpub()); + const hdNode = HDNode.fromBase58(xpub); + this._node0 = hdNode.derive(node); + } + + if (node === 1 && !this._node1) { + const xpub = this.constructor._zpubToXpub(this.getXpub()); + const hdNode = HDNode.fromBase58(xpub); + this._node1 = hdNode.derive(node); + } + + if (node === 0) { + return this._node0.derive(index).publicKey; + } + + if (node === 1) { + return this._node1.derive(index).publicKey; + } + } + + _getExternalAddressByIndex(index) { + return this._getNodeAddressByIndex(0, index); + } + + _getInternalAddressByIndex(index) { + return this._getNodeAddressByIndex(1, index); + } + + /** + * Returning zpub actually, not xpub. Keeping same method name + * for compatibility. + * + * @return {String} zpub + */ + getXpub() { + if (this._xpub) { + return this._xpub; // cache hit + } + // first, getting xpub + const mnemonic = this.secret; + const seed = bip39.mnemonicToSeed(mnemonic); + const root = HDNode.fromSeed(seed); + + const path = "m/84'/0'/0'"; + const child = root.derivePath(path).neutered(); + const xpub = child.toBase58(); + + // bitcoinjs does not support zpub yet, so we just convert it from xpub + let data = b58.decode(xpub); + data = data.slice(4); + data = Buffer.concat([Buffer.from('04b24746', 'hex'), data]); + this._xpub = b58.encode(data); + + return this._xpub; + } + + /** + * @inheritDoc + */ + async fetchTransactions() { + // if txs are absent for some internal address in hierarchy - this is a sign + // we should fetch txs for that address + // OR if some address has unconfirmed balance - should fetch it's txs + // OR some tx for address is unconfirmed + // OR some tx has < 7 confirmations + + // fetching transactions in batch: first, getting batch history for all addresses, + // then batch fetching all involved txids + // finally, batch fetching txids of all inputs (needed to see amounts & addresses of those inputs) + // then we combine it all together + + let addresses2fetch = []; + + for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { + // external addresses first + let hasUnconfirmed = false; + this._txs_by_external_index[c] = this._txs_by_external_index[c] || []; + for (let tx of this._txs_by_external_index[c]) hasUnconfirmed = hasUnconfirmed || !tx.confirmations || tx.confirmations < 7; + + if (hasUnconfirmed || this._txs_by_external_index[c].length === 0 || this._balances_by_external_index[c].u !== 0) { + addresses2fetch.push(this._getExternalAddressByIndex(c)); + } + } + + for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { + // next, internal addresses + let hasUnconfirmed = false; + this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || []; + for (let tx of this._txs_by_internal_index[c]) hasUnconfirmed = hasUnconfirmed || !tx.confirmations || tx.confirmations < 7; + + if (hasUnconfirmed || this._txs_by_internal_index[c].length === 0 || this._balances_by_internal_index[c].u !== 0) { + addresses2fetch.push(this._getInternalAddressByIndex(c)); + } + } + + // first: batch fetch for all addresses histories + let histories = await BlueElectrum.multiGetHistoryByAddress(addresses2fetch); + let txs = {}; + for (let history of Object.values(histories)) { + for (let tx of history) { + txs[tx.tx_hash] = tx; + } + } + + // next, batch fetching each txid we got + let txdatas = await BlueElectrum.multiGetTransactionByTxid(Object.keys(txs)); + + // now, tricky part. we collect all transactions from inputs (vin), and batch fetch them too. + // then we combine all this data (we need inputs to see source addresses and amounts) + let vinTxids = []; + for (let txdata of Object.values(txdatas)) { + for (let vin of txdata.vin) { + vinTxids.push(vin.txid); + } + } + let vintxdatas = await BlueElectrum.multiGetTransactionByTxid(vinTxids); + + // fetched all transactions from our inputs. now we need to combine it. + // iterating all _our_ transactions: + for (let txid of Object.keys(txdatas)) { + // iterating all inputs our our single transaction: + for (let inpNum = 0; inpNum < txdatas[txid].vin.length; inpNum++) { + let inpTxid = txdatas[txid].vin[inpNum].txid; + let inpVout = txdatas[txid].vin[inpNum].vout; + // got txid and output number of _previous_ transaction we shoud look into + if (vintxdatas[inpTxid] && vintxdatas[inpTxid].vout[inpVout]) { + // extracting amount & addresses from previous output and adding it to _our_ input: + txdatas[txid].vin[inpNum].addresses = vintxdatas[inpTxid].vout[inpVout].scriptPubKey.addresses; + txdatas[txid].vin[inpNum].value = vintxdatas[inpTxid].vout[inpVout].value; + } + } + } + + // now purge all unconfirmed txs from internal hashmaps, since some may be evicted from mempool because they became invalid + // or replaced. hashmaps are going to be re-populated anyways, since we fetched TXs for addresses with unconfirmed TXs + for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { + this._txs_by_external_index[c] = this._txs_by_external_index[c].filter(tx => !!tx.confirmations); + } + for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { + this._txs_by_internal_index[c] = this._txs_by_internal_index[c].filter(tx => !!tx.confirmations); + } + + // now, we need to put transactions in all relevant `cells` of internal hashmaps: this._txs_by_internal_index && this._txs_by_external_index + + for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { + for (let tx of Object.values(txdatas)) { + for (let vin of tx.vin) { + if (vin.addresses && vin.addresses.indexOf(this._getExternalAddressByIndex(c)) !== -1) { + // this TX is related to our address + this._txs_by_external_index[c] = this._txs_by_external_index[c] || []; + let clonedTx = Object.assign({}, tx); + clonedTx.inputs = tx.vin.slice(0); + clonedTx.outputs = tx.vout.slice(0); + delete clonedTx.vin; + delete clonedTx.vout; + + // trying to replace tx if it exists already (because it has lower confirmations, for example) + let replaced = false; + for (let cc = 0; cc < this._txs_by_external_index[c].length; cc++) { + if (this._txs_by_external_index[c][cc].txid === clonedTx.txid) { + replaced = true; + this._txs_by_external_index[c][cc] = clonedTx; + } + } + if (!replaced) this._txs_by_external_index[c].push(clonedTx); + } + } + for (let vout of tx.vout) { + if (vout.scriptPubKey.addresses.indexOf(this._getExternalAddressByIndex(c)) !== -1) { + // this TX is related to our address + this._txs_by_external_index[c] = this._txs_by_external_index[c] || []; + let clonedTx = Object.assign({}, tx); + clonedTx.inputs = tx.vin.slice(0); + clonedTx.outputs = tx.vout.slice(0); + delete clonedTx.vin; + delete clonedTx.vout; + + // trying to replace tx if it exists already (because it has lower confirmations, for example) + let replaced = false; + for (let cc = 0; cc < this._txs_by_external_index[c].length; cc++) { + if (this._txs_by_external_index[c][cc].txid === clonedTx.txid) { + replaced = true; + this._txs_by_external_index[c][cc] = clonedTx; + } + } + if (!replaced) this._txs_by_external_index[c].push(clonedTx); + } + } + } + } + + for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { + for (let tx of Object.values(txdatas)) { + for (let vin of tx.vin) { + if (vin.addresses && vin.addresses.indexOf(this._getInternalAddressByIndex(c)) !== -1) { + // this TX is related to our address + this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || []; + let clonedTx = Object.assign({}, tx); + clonedTx.inputs = tx.vin.slice(0); + clonedTx.outputs = tx.vout.slice(0); + delete clonedTx.vin; + delete clonedTx.vout; + + // trying to replace tx if it exists already (because it has lower confirmations, for example) + let replaced = false; + for (let cc = 0; cc < this._txs_by_internal_index[c].length; cc++) { + if (this._txs_by_internal_index[c][cc].txid === clonedTx.txid) { + replaced = true; + this._txs_by_internal_index[c][cc] = clonedTx; + } + } + if (!replaced) this._txs_by_internal_index[c].push(clonedTx); + } + } + for (let vout of tx.vout) { + if (vout.scriptPubKey.addresses.indexOf(this._getInternalAddressByIndex(c)) !== -1) { + // this TX is related to our address + this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || []; + let clonedTx = Object.assign({}, tx); + clonedTx.inputs = tx.vin.slice(0); + clonedTx.outputs = tx.vout.slice(0); + delete clonedTx.vin; + delete clonedTx.vout; + + // trying to replace tx if it exists already (because it has lower confirmations, for example) + let replaced = false; + for (let cc = 0; cc < this._txs_by_internal_index[c].length; cc++) { + if (this._txs_by_internal_index[c][cc].txid === clonedTx.txid) { + replaced = true; + this._txs_by_internal_index[c][cc] = clonedTx; + } + } + if (!replaced) this._txs_by_internal_index[c].push(clonedTx); + } + } + } + } + + this._lastTxFetch = +new Date(); + } + + getTransactions() { + let txs = []; + + for (let addressTxs of Object.values(this._txs_by_external_index)) { + txs = txs.concat(addressTxs); + } + for (let addressTxs of Object.values(this._txs_by_internal_index)) { + txs = txs.concat(addressTxs); + } + + let ret = []; + for (let tx of txs) { + tx.received = tx.blocktime * 1000; + if (!tx.blocktime) tx.received = +new Date() - 30 * 1000; // unconfirmed + tx.confirmations = tx.confirmations || 0; // unconfirmed + tx.hash = tx.txid; + tx.value = 0; + + for (let vin of tx.inputs) { + // if input (spending) goes from our address - we are loosing! + if ((vin.address && this.weOwnAddress(vin.address)) || (vin.addresses && vin.addresses[0] && this.weOwnAddress(vin.addresses[0]))) { + tx.value -= new BigNumber(vin.value).multipliedBy(100000000).toNumber(); + } + } + + for (let vout of tx.outputs) { + // when output goes to our address - this means we are gaining! + if (vout.scriptPubKey.addresses && vout.scriptPubKey.addresses[0] && this.weOwnAddress(vout.scriptPubKey.addresses[0])) { + tx.value += new BigNumber(vout.value).multipliedBy(100000000).toNumber(); + } + } + ret.push(tx); + } + + // now, deduplication: + let usedTxIds = {}; + let ret2 = []; + for (let tx of ret) { + if (!usedTxIds[tx.txid]) ret2.push(tx); + usedTxIds[tx.txid] = 1; + } + + return ret2.sort(function(a, b) { + return b.received - a.received; + }); + } + + async _binarySearchIterationForInternalAddress(index) { + const gerenateChunkAddresses = chunkNum => { + let ret = []; + for (let c = this.gap_limit * chunkNum; c < this.gap_limit * (chunkNum + 1); c++) { + ret.push(this._getInternalAddressByIndex(c)); + } + return ret; + }; + + let lastChunkWithUsedAddressesNum = null; + let lastHistoriesWithUsedAddresses = null; + for (let c = 0; c < Math.round(index / this.gap_limit); c++) { + let histories = await BlueElectrum.multiGetHistoryByAddress(gerenateChunkAddresses(c)); + if (this.constructor._getTransactionsFromHistories(histories).length > 0) { + // in this particular chunk we have used addresses + lastChunkWithUsedAddressesNum = c; + lastHistoriesWithUsedAddresses = histories; + } else { + // empty chunk. no sense searching more chunks + break; + } + } + + let lastUsedIndex = 0; + + if (lastHistoriesWithUsedAddresses) { + // now searching for last used address in batch lastChunkWithUsedAddressesNum + for ( + let c = lastChunkWithUsedAddressesNum * this.gap_limit; + c < lastChunkWithUsedAddressesNum * this.gap_limit + this.gap_limit; + c++ + ) { + let address = this._getInternalAddressByIndex(c); + if (lastHistoriesWithUsedAddresses[address] && lastHistoriesWithUsedAddresses[address].length > 0) { + lastUsedIndex = Math.max(c, lastUsedIndex) + 1; // point to next, which is supposed to be unsued + } + } + } + + return lastUsedIndex; + } + + async _binarySearchIterationForExternalAddress(index) { + const gerenateChunkAddresses = chunkNum => { + let ret = []; + for (let c = this.gap_limit * chunkNum; c < this.gap_limit * (chunkNum + 1); c++) { + ret.push(this._getExternalAddressByIndex(c)); + } + return ret; + }; + + let lastChunkWithUsedAddressesNum = null; + let lastHistoriesWithUsedAddresses = null; + for (let c = 0; c < Math.round(index / this.gap_limit); c++) { + let histories = await BlueElectrum.multiGetHistoryByAddress(gerenateChunkAddresses(c)); + if (this.constructor._getTransactionsFromHistories(histories).length > 0) { + // in this particular chunk we have used addresses + lastChunkWithUsedAddressesNum = c; + lastHistoriesWithUsedAddresses = histories; + } else { + // empty chunk. no sense searching more chunks + break; + } + } + + let lastUsedIndex = 0; + + if (lastHistoriesWithUsedAddresses) { + // now searching for last used address in batch lastChunkWithUsedAddressesNum + for ( + let c = lastChunkWithUsedAddressesNum * this.gap_limit; + c < lastChunkWithUsedAddressesNum * this.gap_limit + this.gap_limit; + c++ + ) { + let address = this._getExternalAddressByIndex(c); + if (lastHistoriesWithUsedAddresses[address] && lastHistoriesWithUsedAddresses[address].length > 0) { + lastUsedIndex = Math.max(c, lastUsedIndex) + 1; // point to next, which is supposed to be unsued + } + } + } + + return lastUsedIndex; + } + + async fetchBalance() { + try { + if (this.next_free_change_address_index === 0 && this.next_free_address_index === 0) { + // doing binary search for last used address: + this.next_free_change_address_index = await this._binarySearchIterationForInternalAddress(1000); + this.next_free_address_index = await this._binarySearchIterationForExternalAddress(1000); + } // end rescanning fresh wallet + + // finally fetching balance + await this._fetchBalance(); + } catch (err) { + console.warn(err); + } + } + + async _fetchBalance() { + // probing future addressess in hierarchy whether they have any transactions, in case + // our 'next free addr' pointers are lagging behind + let tryAgain = false; + let txs = await BlueElectrum.getTransactionsByAddress( + this._getExternalAddressByIndex(this.next_free_address_index + this.gap_limit - 1), + ); + if (txs.length > 0) { + // whoa, someone uses our wallet outside! better catch up + this.next_free_address_index += this.gap_limit; + tryAgain = true; + } + + txs = await BlueElectrum.getTransactionsByAddress( + this._getInternalAddressByIndex(this.next_free_change_address_index + this.gap_limit - 1), + ); + if (txs.length > 0) { + this.next_free_change_address_index += this.gap_limit; + tryAgain = true; + } + + // FIXME: refactor me ^^^ can be batched in single call. plus not just couple of addresses, but all between [ next_free .. (next_free + gap_limit) ] + + if (tryAgain) return this._fetchBalance(); + + // next, business as usuall. fetch balances + + let addresses2fetch = []; + + // generating all involved addresses. + // basically, refetch all from index zero to maximum. doesnt matter + // since we batch them 100 per call + + // external + for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { + addresses2fetch.push(this._getExternalAddressByIndex(c)); + } + + // internal + for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { + addresses2fetch.push(this._getInternalAddressByIndex(c)); + } + + let balances = await BlueElectrum.multiGetBalanceByAddress(addresses2fetch); + + // converting to a more compact internal format + for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { + let addr = this._getExternalAddressByIndex(c); + if (balances.addresses[addr]) { + // first, if balances differ from what we store - we delete transactions for that + // address so next fetchTransactions() will refetch everything + if (this._balances_by_external_index[c]) { + if ( + this._balances_by_external_index[c].c !== balances.addresses[addr].confirmed || + this._balances_by_external_index[c].u !== balances.addresses[addr].unconfirmed + ) { + delete this._txs_by_external_index[c]; + } + } + // update local representation of balances on that address: + this._balances_by_external_index[c] = { + c: balances.addresses[addr].confirmed, + u: balances.addresses[addr].unconfirmed, + }; + } + } + for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { + let addr = this._getInternalAddressByIndex(c); + if (balances.addresses[addr]) { + // first, if balances differ from what we store - we delete transactions for that + // address so next fetchTransactions() will refetch everything + if (this._balances_by_internal_index[c]) { + if ( + this._balances_by_internal_index[c].c !== balances.addresses[addr].confirmed || + this._balances_by_internal_index[c].u !== balances.addresses[addr].unconfirmed + ) { + delete this._txs_by_internal_index[c]; + } + } + // update local representation of balances on that address: + this._balances_by_internal_index[c] = { + c: balances.addresses[addr].confirmed, + u: balances.addresses[addr].unconfirmed, + }; + } + } + + this._lastBalanceFetch = +new Date(); + } + + async fetchUtxo() { + // considering only confirmed balance + let addressess = []; + + for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { + if (this._balances_by_external_index[c] && this._balances_by_external_index[c].c && this._balances_by_external_index[c].c > 0) { + addressess.push(this._getExternalAddressByIndex(c)); + } + } + + for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { + if (this._balances_by_internal_index[c] && this._balances_by_internal_index[c].c && this._balances_by_internal_index[c].c > 0) { + addressess.push(this._getInternalAddressByIndex(c)); + } + } + + this._utxo = []; + for (let arr of Object.values(await BlueElectrum.multiGetUtxoByAddress(addressess))) { + this._utxo = this._utxo.concat(arr); + } + + // backward compatibility TODO: remove when we make sure `.utxo` is not used + this.utxo = this._utxo; + // this belongs in `.getUtxo()` + for (let u of this.utxo) { + u.txid = u.txId; + u.amount = u.value; + u.wif = this._getWifForAddress(u.address); + u.confirmations = u.height ? 1 : 0; + } + } + + getUtxo() { + return this._utxo; + } + + _getDerivationPathByAddress(address) { + const path = "m/84'/0'/0'"; + for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { + if (this._getExternalAddressByIndex(c) === address) return path + '/0/' + c; + } + for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { + if (this._getInternalAddressByIndex(c) === address) return path + '/1/' + c; + } + + return false; + } + + _getPubkeyByAddress(address) { + for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { + if (this._getExternalAddressByIndex(c) === address) return this._getNodePubkeyByIndex(0, c); + } + for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { + if (this._getInternalAddressByIndex(c) === address) return this._getNodePubkeyByIndex(1, c); + } + + return false; + } + + weOwnAddress(address) { + for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { + if (this._getExternalAddressByIndex(c) === address) return true; + } + for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { + if (this._getInternalAddressByIndex(c) === address) return true; + } + return false; + } + + /** + * @deprecated + */ + createTx(utxos, amount, fee, address) { + throw new Error('Deprecated'); + } + + /** + * + * @param utxos {Array.<{vout: Number, value: Number, txId: String, address: String}>} List of spendable utxos + * @param targets {Array.<{value: Number, address: String}>} Where coins are going. If theres only 1 target and that target has no value - this will send MAX to that address (respecting fee rate) + * @param feeRate {Number} satoshi per byte + * @param changeAddress {String} Excessive coins will go back to that address + * @param sequence {Number} Used in RBF + * @param skipSigning {boolean} Whether we should skip signing, use returned `psbt` in that case + * @returns {{outputs: Array, tx: Transaction, inputs: Array, fee: Number, psbt: Psbt}} + */ + createTransaction(utxos, targets, feeRate, changeAddress, sequence, skipSigning = false) { + if (!changeAddress) throw new Error('No change address provided'); + sequence = sequence || AbstractHDElectrumWallet.defaultRBFSequence; + + let algo = coinSelectAccumulative; + if (targets.length === 1 && targets[0] && !targets[0].value) { + // we want to send MAX + algo = coinSelectSplit; + } + + let { inputs, outputs, fee } = algo(utxos, targets, feeRate); + + // .inputs and .outputs will be undefined if no solution was found + if (!inputs || !outputs) { + throw new Error('Not enough balance. Try sending smaller amount'); + } + + let psbt = new bitcoin.Psbt(); + + let c = 0; + let keypairs = {}; + let values = {}; + + inputs.forEach(input => { + let keyPair; + if (!skipSigning) { + // skiping signing related stuff + keyPair = bitcoin.ECPair.fromWIF(this._getWifForAddress(input.address)); + keypairs[c] = keyPair; + } + values[c] = input.value; + c++; + if (!skipSigning) { + // skiping signing related stuff + if (!input.address || !this._getWifForAddress(input.address)) throw new Error('Internal error: no address or WIF to sign input'); + } + let pubkey = this._getPubkeyByAddress(input.address); + let masterFingerprint = Buffer.from([0x00, 0x00, 0x00, 0x00]); + // this is not correct fingerprint, as we dont know real fingerprint - we got zpub with 84/0, but fingerpting + // should be from root. basically, fingerprint should be provided from outside by user when importing zpub + let path = this._getDerivationPathByAddress(input.address); + const p2wpkh = bitcoin.payments.p2wpkh({ pubkey }); + psbt.addInput({ + hash: input.txId, + index: input.vout, + sequence, + bip32Derivation: [ + { + masterFingerprint, + path, + pubkey, + }, + ], + witnessUtxo: { + script: p2wpkh.output, + value: input.value, + }, + }); + }); + + outputs.forEach(output => { + // if output has no address - this is change output + let change = false; + if (!output.address) { + change = true; + output.address = changeAddress; + } + + let path = this._getDerivationPathByAddress(output.address); + let pubkey = this._getPubkeyByAddress(output.address); + let masterFingerprint = Buffer.from([0x00, 0x00, 0x00, 0x00]); + // this is not correct fingerprint, as we dont know realfingerprint - we got zpub with 84/0, but fingerpting + // should be from root. basically, fingerprint should be provided from outside by user when importing zpub + + let outputData = { + address: output.address, + value: output.value, + }; + + if (change) { + outputData['bip32Derivation'] = [ + { + masterFingerprint, + path, + pubkey, + }, + ]; + } + + psbt.addOutput(outputData); + }); + + if (!skipSigning) { + // skiping signing related stuff + for (let cc = 0; cc < c; cc++) { + psbt.signInput(cc, keypairs[cc]); + } + } + + let tx; + if (!skipSigning) { + tx = psbt.finalizeAllInputs().extractTransaction(); + } + return { tx, inputs, outputs, fee, psbt }; + } + + /** + * Combines 2 PSBTs into final transaction from which you can + * get HEX and broadcast + * + * @param base64one {string} + * @param base64two {string} + * @returns {Transaction} + */ + combinePsbt(base64one, base64two) { + const final1 = bitcoin.Psbt.fromBase64(base64one); + const final2 = bitcoin.Psbt.fromBase64(base64two); + final1.combine(final2); + return final1.finalizeAllInputs().extractTransaction(); + } + + /** + * Creates Segwit Bech32 Bitcoin address + * + * @param hdNode + * @returns {String} + */ + static _nodeToBech32SegwitAddress(hdNode) { + return bitcoin.payments.p2wpkh({ + pubkey: hdNode.publicKey, + }).address; + } + + /** + * Converts zpub to xpub + * + * @param {String} zpub + * @returns {String} xpub + */ + static _zpubToXpub(zpub) { + let data = b58.decode(zpub); + data = data.slice(4); + data = Buffer.concat([Buffer.from('0488b21e', 'hex'), data]); + + return b58.encode(data); + } + + static _getTransactionsFromHistories(histories) { + let txs = []; + for (let history of Object.values(histories)) { + for (let tx of history) { + txs.push(tx); + } + } + return txs; + } + + /** + * Broadcast txhex. Can throw an exception if failed + * + * @param {String} txhex + * @returns {Promise} + */ + async broadcastTx(txhex) { + let broadcast = await BlueElectrum.broadcastV2(txhex); + console.log({ broadcast }); + if (broadcast.indexOf('successfully') !== -1) return true; + return broadcast.length === 64; // this means return string is txid (precise length), so it was broadcasted ok + } +} diff --git a/class/hd-segwit-bech32-wallet.js b/class/hd-segwit-bech32-wallet.js index 00b3eb2d..fe277687 100644 --- a/class/hd-segwit-bech32-wallet.js +++ b/class/hd-segwit-bech32-wallet.js @@ -1,898 +1,23 @@ -import { AbstractHDWallet } from './abstract-hd-wallet'; -import { NativeModules } from 'react-native'; -import bip39 from 'bip39'; -import BigNumber from 'bignumber.js'; -import b58 from 'bs58check'; -const bitcoin = require('bitcoinjs-lib'); -const BlueElectrum = require('../BlueElectrum'); -const HDNode = require('bip32'); -const coinSelectAccumulative = require('coinselect/accumulative'); -const coinSelectSplit = require('coinselect/split'); - -const { RNRandomBytes } = NativeModules; +import { AbstractHDElectrumWallet } from './abstract-hd-electrum-wallet'; /** * HD Wallet (BIP39). * In particular, BIP84 (Bech32 Native Segwit) * @see https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki */ -export class HDSegwitBech32Wallet extends AbstractHDWallet { +export class HDSegwitBech32Wallet extends AbstractHDElectrumWallet { static type = 'HDsegwitBech32'; static typeReadable = 'HD SegWit (BIP84 Bech32 Native)'; - static defaultRBFSequence = 2147483648; // 1 << 31, minimum for replaceable transactions as per BIP68 - static finalRBFSequence = 4294967295; // 0xFFFFFFFF - - constructor() { - super(); - this._balances_by_external_index = {}; // 0 => { c: 0, u: 0 } // confirmed/unconfirmed - this._balances_by_internal_index = {}; - - this._txs_by_external_index = {}; - this._txs_by_internal_index = {}; - this._utxo = []; + allowSend() { + return true; } allowBatchSend() { return true; } - allowSendMax(): boolean { + allowSendMax() { return true; } - - /** - * @inheritDoc - */ - getBalance() { - let ret = 0; - for (let bal of Object.values(this._balances_by_external_index)) { - ret += bal.c; - } - for (let bal of Object.values(this._balances_by_internal_index)) { - ret += bal.c; - } - return ret + (this.getUnconfirmedBalance() < 0 ? this.getUnconfirmedBalance() : 0); - } - - /** - * @inheritDoc - */ - timeToRefreshTransaction() { - for (let tx of this.getTransactions()) { - if (tx.confirmations < 7) return true; - } - return false; - } - - getUnconfirmedBalance() { - let ret = 0; - for (let bal of Object.values(this._balances_by_external_index)) { - ret += bal.u; - } - for (let bal of Object.values(this._balances_by_internal_index)) { - ret += bal.u; - } - return ret; - } - - allowSend() { - return true; - } - - async generate() { - let that = this; - return new Promise(function(resolve) { - if (typeof RNRandomBytes === 'undefined') { - // CLI/CI environment - // crypto should be provided globally by test launcher - return crypto.randomBytes(32, (err, buf) => { // eslint-disable-line - if (err) throw err; - that.secret = bip39.entropyToMnemonic(buf.toString('hex')); - resolve(); - }); - } - - // RN environment - RNRandomBytes.randomBytes(32, (err, bytes) => { - if (err) throw new Error(err); - let b = Buffer.from(bytes, 'base64').toString('hex'); - that.secret = bip39.entropyToMnemonic(b); - resolve(); - }); - }); - } - - _getExternalWIFByIndex(index) { - return this._getWIFByIndex(false, index); - } - - _getInternalWIFByIndex(index) { - return this._getWIFByIndex(true, index); - } - - /** - * Get internal/external WIF by wallet index - * @param {Boolean} internal - * @param {Number} index - * @returns {string|false} Either string WIF or FALSE if error happened - * @private - */ - _getWIFByIndex(internal, index) { - if (!this.secret) return false; - const mnemonic = this.secret; - const seed = bip39.mnemonicToSeed(mnemonic); - const root = HDNode.fromSeed(seed); - const path = `m/84'/0'/0'/${internal ? 1 : 0}/${index}`; - const child = root.derivePath(path); - - return child.toWIF(); - } - - _getNodeAddressByIndex(node, index) { - index = index * 1; // cast to int - if (node === 0) { - if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit - } - - if (node === 1) { - if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit - } - - if (node === 0 && !this._node0) { - const xpub = this.constructor._zpubToXpub(this.getXpub()); - const hdNode = HDNode.fromBase58(xpub); - this._node0 = hdNode.derive(node); - } - - if (node === 1 && !this._node1) { - const xpub = this.constructor._zpubToXpub(this.getXpub()); - const hdNode = HDNode.fromBase58(xpub); - this._node1 = hdNode.derive(node); - } - - let address; - if (node === 0) { - address = this.constructor._nodeToBech32SegwitAddress(this._node0.derive(index)); - } - - if (node === 1) { - address = this.constructor._nodeToBech32SegwitAddress(this._node1.derive(index)); - } - - if (node === 0) { - return (this.external_addresses_cache[index] = address); - } - - if (node === 1) { - return (this.internal_addresses_cache[index] = address); - } - } - - _getNodePubkeyByIndex(node, index) { - index = index * 1; // cast to int - - if (node === 0 && !this._node0) { - const xpub = this.constructor._zpubToXpub(this.getXpub()); - const hdNode = HDNode.fromBase58(xpub); - this._node0 = hdNode.derive(node); - } - - if (node === 1 && !this._node1) { - const xpub = this.constructor._zpubToXpub(this.getXpub()); - const hdNode = HDNode.fromBase58(xpub); - this._node1 = hdNode.derive(node); - } - - if (node === 0) { - return this._node0.derive(index).publicKey; - } - - if (node === 1) { - return this._node1.derive(index).publicKey; - } - } - - _getExternalAddressByIndex(index) { - return this._getNodeAddressByIndex(0, index); - } - - _getInternalAddressByIndex(index) { - return this._getNodeAddressByIndex(1, index); - } - - /** - * Returning zpub actually, not xpub. Keeping same method name - * for compatibility. - * - * @return {String} zpub - */ - getXpub() { - if (this._xpub) { - return this._xpub; // cache hit - } - // first, getting xpub - const mnemonic = this.secret; - const seed = bip39.mnemonicToSeed(mnemonic); - const root = HDNode.fromSeed(seed); - - const path = "m/84'/0'/0'"; - const child = root.derivePath(path).neutered(); - const xpub = child.toBase58(); - - // bitcoinjs does not support zpub yet, so we just convert it from xpub - let data = b58.decode(xpub); - data = data.slice(4); - data = Buffer.concat([Buffer.from('04b24746', 'hex'), data]); - this._xpub = b58.encode(data); - - return this._xpub; - } - - /** - * @inheritDoc - */ - async fetchTransactions() { - // if txs are absent for some internal address in hierarchy - this is a sign - // we should fetch txs for that address - // OR if some address has unconfirmed balance - should fetch it's txs - // OR some tx for address is unconfirmed - // OR some tx has < 7 confirmations - - // fetching transactions in batch: first, getting batch history for all addresses, - // then batch fetching all involved txids - // finally, batch fetching txids of all inputs (needed to see amounts & addresses of those inputs) - // then we combine it all together - - let addresses2fetch = []; - - for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { - // external addresses first - let hasUnconfirmed = false; - this._txs_by_external_index[c] = this._txs_by_external_index[c] || []; - for (let tx of this._txs_by_external_index[c]) hasUnconfirmed = hasUnconfirmed || !tx.confirmations || tx.confirmations < 7; - - if (hasUnconfirmed || this._txs_by_external_index[c].length === 0 || this._balances_by_external_index[c].u !== 0) { - addresses2fetch.push(this._getExternalAddressByIndex(c)); - } - } - - for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { - // next, internal addresses - let hasUnconfirmed = false; - this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || []; - for (let tx of this._txs_by_internal_index[c]) hasUnconfirmed = hasUnconfirmed || !tx.confirmations || tx.confirmations < 7; - - if (hasUnconfirmed || this._txs_by_internal_index[c].length === 0 || this._balances_by_internal_index[c].u !== 0) { - addresses2fetch.push(this._getInternalAddressByIndex(c)); - } - } - - // first: batch fetch for all addresses histories - let histories = await BlueElectrum.multiGetHistoryByAddress(addresses2fetch); - let txs = {}; - for (let history of Object.values(histories)) { - for (let tx of history) { - txs[tx.tx_hash] = tx; - } - } - - // next, batch fetching each txid we got - let txdatas = await BlueElectrum.multiGetTransactionByTxid(Object.keys(txs)); - - // now, tricky part. we collect all transactions from inputs (vin), and batch fetch them too. - // then we combine all this data (we need inputs to see source addresses and amounts) - let vinTxids = []; - for (let txdata of Object.values(txdatas)) { - for (let vin of txdata.vin) { - vinTxids.push(vin.txid); - } - } - let vintxdatas = await BlueElectrum.multiGetTransactionByTxid(vinTxids); - - // fetched all transactions from our inputs. now we need to combine it. - // iterating all _our_ transactions: - for (let txid of Object.keys(txdatas)) { - // iterating all inputs our our single transaction: - for (let inpNum = 0; inpNum < txdatas[txid].vin.length; inpNum++) { - let inpTxid = txdatas[txid].vin[inpNum].txid; - let inpVout = txdatas[txid].vin[inpNum].vout; - // got txid and output number of _previous_ transaction we shoud look into - if (vintxdatas[inpTxid] && vintxdatas[inpTxid].vout[inpVout]) { - // extracting amount & addresses from previous output and adding it to _our_ input: - txdatas[txid].vin[inpNum].addresses = vintxdatas[inpTxid].vout[inpVout].scriptPubKey.addresses; - txdatas[txid].vin[inpNum].value = vintxdatas[inpTxid].vout[inpVout].value; - } - } - } - - // now purge all unconfirmed txs from internal hashmaps, since some may be evicted from mempool because they became invalid - // or replaced. hashmaps are going to be re-populated anyways, since we fetched TXs for addresses with unconfirmed TXs - for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { - this._txs_by_external_index[c] = this._txs_by_external_index[c].filter(tx => !!tx.confirmations); - } - for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { - this._txs_by_internal_index[c] = this._txs_by_internal_index[c].filter(tx => !!tx.confirmations); - } - - // now, we need to put transactions in all relevant `cells` of internal hashmaps: this._txs_by_internal_index && this._txs_by_external_index - - for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { - for (let tx of Object.values(txdatas)) { - for (let vin of tx.vin) { - if (vin.addresses && vin.addresses.indexOf(this._getExternalAddressByIndex(c)) !== -1) { - // this TX is related to our address - this._txs_by_external_index[c] = this._txs_by_external_index[c] || []; - let clonedTx = Object.assign({}, tx); - clonedTx.inputs = tx.vin.slice(0); - clonedTx.outputs = tx.vout.slice(0); - delete clonedTx.vin; - delete clonedTx.vout; - - // trying to replace tx if it exists already (because it has lower confirmations, for example) - let replaced = false; - for (let cc = 0; cc < this._txs_by_external_index[c].length; cc++) { - if (this._txs_by_external_index[c][cc].txid === clonedTx.txid) { - replaced = true; - this._txs_by_external_index[c][cc] = clonedTx; - } - } - if (!replaced) this._txs_by_external_index[c].push(clonedTx); - } - } - for (let vout of tx.vout) { - if (vout.scriptPubKey.addresses.indexOf(this._getExternalAddressByIndex(c)) !== -1) { - // this TX is related to our address - this._txs_by_external_index[c] = this._txs_by_external_index[c] || []; - let clonedTx = Object.assign({}, tx); - clonedTx.inputs = tx.vin.slice(0); - clonedTx.outputs = tx.vout.slice(0); - delete clonedTx.vin; - delete clonedTx.vout; - - // trying to replace tx if it exists already (because it has lower confirmations, for example) - let replaced = false; - for (let cc = 0; cc < this._txs_by_external_index[c].length; cc++) { - if (this._txs_by_external_index[c][cc].txid === clonedTx.txid) { - replaced = true; - this._txs_by_external_index[c][cc] = clonedTx; - } - } - if (!replaced) this._txs_by_external_index[c].push(clonedTx); - } - } - } - } - - for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { - for (let tx of Object.values(txdatas)) { - for (let vin of tx.vin) { - if (vin.addresses && vin.addresses.indexOf(this._getInternalAddressByIndex(c)) !== -1) { - // this TX is related to our address - this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || []; - let clonedTx = Object.assign({}, tx); - clonedTx.inputs = tx.vin.slice(0); - clonedTx.outputs = tx.vout.slice(0); - delete clonedTx.vin; - delete clonedTx.vout; - - // trying to replace tx if it exists already (because it has lower confirmations, for example) - let replaced = false; - for (let cc = 0; cc < this._txs_by_internal_index[c].length; cc++) { - if (this._txs_by_internal_index[c][cc].txid === clonedTx.txid) { - replaced = true; - this._txs_by_internal_index[c][cc] = clonedTx; - } - } - if (!replaced) this._txs_by_internal_index[c].push(clonedTx); - } - } - for (let vout of tx.vout) { - if (vout.scriptPubKey.addresses.indexOf(this._getInternalAddressByIndex(c)) !== -1) { - // this TX is related to our address - this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || []; - let clonedTx = Object.assign({}, tx); - clonedTx.inputs = tx.vin.slice(0); - clonedTx.outputs = tx.vout.slice(0); - delete clonedTx.vin; - delete clonedTx.vout; - - // trying to replace tx if it exists already (because it has lower confirmations, for example) - let replaced = false; - for (let cc = 0; cc < this._txs_by_internal_index[c].length; cc++) { - if (this._txs_by_internal_index[c][cc].txid === clonedTx.txid) { - replaced = true; - this._txs_by_internal_index[c][cc] = clonedTx; - } - } - if (!replaced) this._txs_by_internal_index[c].push(clonedTx); - } - } - } - } - - this._lastTxFetch = +new Date(); - } - - getTransactions() { - let txs = []; - - for (let addressTxs of Object.values(this._txs_by_external_index)) { - txs = txs.concat(addressTxs); - } - for (let addressTxs of Object.values(this._txs_by_internal_index)) { - txs = txs.concat(addressTxs); - } - - let ret = []; - for (let tx of txs) { - tx.received = tx.blocktime * 1000; - if (!tx.blocktime) tx.received = +new Date() - 30 * 1000; // unconfirmed - tx.confirmations = tx.confirmations || 0; // unconfirmed - tx.hash = tx.txid; - tx.value = 0; - - for (let vin of tx.inputs) { - // if input (spending) goes from our address - we are loosing! - if ((vin.address && this.weOwnAddress(vin.address)) || (vin.addresses && vin.addresses[0] && this.weOwnAddress(vin.addresses[0]))) { - tx.value -= new BigNumber(vin.value).multipliedBy(100000000).toNumber(); - } - } - - for (let vout of tx.outputs) { - // when output goes to our address - this means we are gaining! - if (vout.scriptPubKey.addresses && vout.scriptPubKey.addresses[0] && this.weOwnAddress(vout.scriptPubKey.addresses[0])) { - tx.value += new BigNumber(vout.value).multipliedBy(100000000).toNumber(); - } - } - ret.push(tx); - } - - // now, deduplication: - let usedTxIds = {}; - let ret2 = []; - for (let tx of ret) { - if (!usedTxIds[tx.txid]) ret2.push(tx); - usedTxIds[tx.txid] = 1; - } - - return ret2.sort(function(a, b) { - return b.received - a.received; - }); - } - - async _binarySearchIterationForInternalAddress(index) { - const gerenateChunkAddresses = chunkNum => { - let ret = []; - for (let c = this.gap_limit * chunkNum; c < this.gap_limit * (chunkNum + 1); c++) { - ret.push(this._getInternalAddressByIndex(c)); - } - return ret; - }; - - let lastChunkWithUsedAddressesNum = null; - let lastHistoriesWithUsedAddresses = null; - for (let c = 0; c < Math.round(index / this.gap_limit); c++) { - let histories = await BlueElectrum.multiGetHistoryByAddress(gerenateChunkAddresses(c)); - if (this.constructor._getTransactionsFromHistories(histories).length > 0) { - // in this particular chunk we have used addresses - lastChunkWithUsedAddressesNum = c; - lastHistoriesWithUsedAddresses = histories; - } else { - // empty chunk. no sense searching more chunks - break; - } - } - - let lastUsedIndex = 0; - - if (lastHistoriesWithUsedAddresses) { - // now searching for last used address in batch lastChunkWithUsedAddressesNum - for ( - let c = lastChunkWithUsedAddressesNum * this.gap_limit; - c < lastChunkWithUsedAddressesNum * this.gap_limit + this.gap_limit; - c++ - ) { - let address = this._getInternalAddressByIndex(c); - if (lastHistoriesWithUsedAddresses[address] && lastHistoriesWithUsedAddresses[address].length > 0) { - lastUsedIndex = Math.max(c, lastUsedIndex) + 1; // point to next, which is supposed to be unsued - } - } - } - - return lastUsedIndex; - } - - async _binarySearchIterationForExternalAddress(index) { - const gerenateChunkAddresses = chunkNum => { - let ret = []; - for (let c = this.gap_limit * chunkNum; c < this.gap_limit * (chunkNum + 1); c++) { - ret.push(this._getExternalAddressByIndex(c)); - } - return ret; - }; - - let lastChunkWithUsedAddressesNum = null; - let lastHistoriesWithUsedAddresses = null; - for (let c = 0; c < Math.round(index / this.gap_limit); c++) { - let histories = await BlueElectrum.multiGetHistoryByAddress(gerenateChunkAddresses(c)); - if (this.constructor._getTransactionsFromHistories(histories).length > 0) { - // in this particular chunk we have used addresses - lastChunkWithUsedAddressesNum = c; - lastHistoriesWithUsedAddresses = histories; - } else { - // empty chunk. no sense searching more chunks - break; - } - } - - let lastUsedIndex = 0; - - if (lastHistoriesWithUsedAddresses) { - // now searching for last used address in batch lastChunkWithUsedAddressesNum - for ( - let c = lastChunkWithUsedAddressesNum * this.gap_limit; - c < lastChunkWithUsedAddressesNum * this.gap_limit + this.gap_limit; - c++ - ) { - let address = this._getExternalAddressByIndex(c); - if (lastHistoriesWithUsedAddresses[address] && lastHistoriesWithUsedAddresses[address].length > 0) { - lastUsedIndex = Math.max(c, lastUsedIndex) + 1; // point to next, which is supposed to be unsued - } - } - } - - return lastUsedIndex; - } - - async fetchBalance() { - try { - if (this.next_free_change_address_index === 0 && this.next_free_address_index === 0) { - // doing binary search for last used address: - this.next_free_change_address_index = await this._binarySearchIterationForInternalAddress(1000); - this.next_free_address_index = await this._binarySearchIterationForExternalAddress(1000); - } // end rescanning fresh wallet - - // finally fetching balance - await this._fetchBalance(); - } catch (err) { - console.warn(err); - } - } - - async _fetchBalance() { - // probing future addressess in hierarchy whether they have any transactions, in case - // our 'next free addr' pointers are lagging behind - let tryAgain = false; - let txs = await BlueElectrum.getTransactionsByAddress( - this._getExternalAddressByIndex(this.next_free_address_index + this.gap_limit - 1), - ); - if (txs.length > 0) { - // whoa, someone uses our wallet outside! better catch up - this.next_free_address_index += this.gap_limit; - tryAgain = true; - } - - txs = await BlueElectrum.getTransactionsByAddress( - this._getInternalAddressByIndex(this.next_free_change_address_index + this.gap_limit - 1), - ); - if (txs.length > 0) { - this.next_free_change_address_index += this.gap_limit; - tryAgain = true; - } - - // FIXME: refactor me ^^^ can be batched in single call. plus not just couple of addresses, but all between [ next_free .. (next_free + gap_limit) ] - - if (tryAgain) return this._fetchBalance(); - - // next, business as usuall. fetch balances - - let addresses2fetch = []; - - // generating all involved addresses. - // basically, refetch all from index zero to maximum. doesnt matter - // since we batch them 100 per call - - // external - for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { - addresses2fetch.push(this._getExternalAddressByIndex(c)); - } - - // internal - for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { - addresses2fetch.push(this._getInternalAddressByIndex(c)); - } - - let balances = await BlueElectrum.multiGetBalanceByAddress(addresses2fetch); - - // converting to a more compact internal format - for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { - let addr = this._getExternalAddressByIndex(c); - if (balances.addresses[addr]) { - // first, if balances differ from what we store - we delete transactions for that - // address so next fetchTransactions() will refetch everything - if (this._balances_by_external_index[c]) { - if ( - this._balances_by_external_index[c].c !== balances.addresses[addr].confirmed || - this._balances_by_external_index[c].u !== balances.addresses[addr].unconfirmed - ) { - delete this._txs_by_external_index[c]; - } - } - // update local representation of balances on that address: - this._balances_by_external_index[c] = { - c: balances.addresses[addr].confirmed, - u: balances.addresses[addr].unconfirmed, - }; - } - } - for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { - let addr = this._getInternalAddressByIndex(c); - if (balances.addresses[addr]) { - // first, if balances differ from what we store - we delete transactions for that - // address so next fetchTransactions() will refetch everything - if (this._balances_by_internal_index[c]) { - if ( - this._balances_by_internal_index[c].c !== balances.addresses[addr].confirmed || - this._balances_by_internal_index[c].u !== balances.addresses[addr].unconfirmed - ) { - delete this._txs_by_internal_index[c]; - } - } - // update local representation of balances on that address: - this._balances_by_internal_index[c] = { - c: balances.addresses[addr].confirmed, - u: balances.addresses[addr].unconfirmed, - }; - } - } - - this._lastBalanceFetch = +new Date(); - } - - async fetchUtxo() { - // considering only confirmed balance - let addressess = []; - - for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { - if (this._balances_by_external_index[c] && this._balances_by_external_index[c].c && this._balances_by_external_index[c].c > 0) { - addressess.push(this._getExternalAddressByIndex(c)); - } - } - - for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { - if (this._balances_by_internal_index[c] && this._balances_by_internal_index[c].c && this._balances_by_internal_index[c].c > 0) { - addressess.push(this._getInternalAddressByIndex(c)); - } - } - - this._utxo = []; - for (let arr of Object.values(await BlueElectrum.multiGetUtxoByAddress(addressess))) { - this._utxo = this._utxo.concat(arr); - } - } - - getUtxo() { - return this._utxo; - } - - _getDerivationPathByAddress(address) { - const path = "m/84'/0'/0'"; - for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { - if (this._getExternalAddressByIndex(c) === address) return path + '/0/' + c; - } - for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { - if (this._getInternalAddressByIndex(c) === address) return path + '/1/' + c; - } - - return false; - } - - _getPubkeyByAddress(address) { - for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { - if (this._getExternalAddressByIndex(c) === address) return this._getNodePubkeyByIndex(0, c); - } - for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { - if (this._getInternalAddressByIndex(c) === address) return this._getNodePubkeyByIndex(1, c); - } - - return false; - } - - weOwnAddress(address) { - for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { - if (this._getExternalAddressByIndex(c) === address) return true; - } - for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { - if (this._getInternalAddressByIndex(c) === address) return true; - } - return false; - } - - /** - * @deprecated - */ - createTx(utxos, amount, fee, address) { - throw new Error('Deprecated'); - } - - /** - * - * @param utxos {Array.<{vout: Number, value: Number, txId: String, address: String}>} List of spendable utxos - * @param targets {Array.<{value: Number, address: String}>} Where coins are going. If theres only 1 target and that target has no value - this will send MAX to that address (respecting fee rate) - * @param feeRate {Number} satoshi per byte - * @param changeAddress {String} Excessive coins will go back to that address - * @param sequence {Number} Used in RBF - * @param skipSigning {boolean} Whether we should skip signing, use returned `psbt` in that case - * @returns {{outputs: Array, tx: Transaction, inputs: Array, fee: Number, psbt: Psbt}} - */ - createTransaction(utxos, targets, feeRate, changeAddress, sequence, skipSigning = false) { - if (!changeAddress) throw new Error('No change address provided'); - sequence = sequence || HDSegwitBech32Wallet.defaultRBFSequence; - - let algo = coinSelectAccumulative; - if (targets.length === 1 && targets[0] && !targets[0].value) { - // we want to send MAX - algo = coinSelectSplit; - } - - let { inputs, outputs, fee } = algo(utxos, targets, feeRate); - - // .inputs and .outputs will be undefined if no solution was found - if (!inputs || !outputs) { - throw new Error('Not enough balance. Try sending smaller amount'); - } - - let psbt = new bitcoin.Psbt(); - - let c = 0; - let keypairs = {}; - let values = {}; - - inputs.forEach(input => { - let keyPair; - if (!skipSigning) { - // skiping signing related stuff - keyPair = bitcoin.ECPair.fromWIF(this._getWifForAddress(input.address)); - keypairs[c] = keyPair; - } - values[c] = input.value; - c++; - if (!skipSigning) { - // skiping signing related stuff - if (!input.address || !this._getWifForAddress(input.address)) throw new Error('Internal error: no address or WIF to sign input'); - } - let pubkey = this._getPubkeyByAddress(input.address); - let masterFingerprint = Buffer.from([0x00, 0x00, 0x00, 0x00]); - // this is not correct fingerprint, as we dont know real fingerprint - we got zpub with 84/0, but fingerpting - // should be from root. basically, fingerprint should be provided from outside by user when importing zpub - let path = this._getDerivationPathByAddress(input.address); - const p2wpkh = bitcoin.payments.p2wpkh({ pubkey }); - psbt.addInput({ - hash: input.txId, - index: input.vout, - sequence, - bip32Derivation: [ - { - masterFingerprint, - path, - pubkey, - }, - ], - witnessUtxo: { - script: p2wpkh.output, - value: input.value, - }, - }); - }); - - outputs.forEach(output => { - // if output has no address - this is change output - let change = false; - if (!output.address) { - change = true; - output.address = changeAddress; - } - - let path = this._getDerivationPathByAddress(output.address); - let pubkey = this._getPubkeyByAddress(output.address); - let masterFingerprint = Buffer.from([0x00, 0x00, 0x00, 0x00]); - // this is not correct fingerprint, as we dont know realfingerprint - we got zpub with 84/0, but fingerpting - // should be from root. basically, fingerprint should be provided from outside by user when importing zpub - - let outputData = { - address: output.address, - value: output.value, - }; - - if (change) { - outputData['bip32Derivation'] = [ - { - masterFingerprint, - path, - pubkey, - }, - ]; - } - - psbt.addOutput(outputData); - }); - - if (!skipSigning) { - // skiping signing related stuff - for (let cc = 0; cc < c; cc++) { - psbt.signInput(cc, keypairs[cc]); - } - } - - let tx; - if (!skipSigning) { - tx = psbt.finalizeAllInputs().extractTransaction(); - } - return { tx, inputs, outputs, fee, psbt }; - } - - /** - * Combines 2 PSBTs into final transaction from which you can - * get HEX and broadcast - * - * @param base64one {string} - * @param base64two {string} - * @returns {Transaction} - */ - combinePsbt(base64one, base64two) { - const final1 = bitcoin.Psbt.fromBase64(base64one); - const final2 = bitcoin.Psbt.fromBase64(base64two); - final1.combine(final2); - return final1.finalizeAllInputs().extractTransaction(); - } - - /** - * Creates Segwit Bech32 Bitcoin address - * - * @param hdNode - * @returns {String} - */ - static _nodeToBech32SegwitAddress(hdNode) { - return bitcoin.payments.p2wpkh({ - pubkey: hdNode.publicKey, - }).address; - } - - /** - * Converts zpub to xpub - * - * @param {String} zpub - * @returns {String} xpub - */ - static _zpubToXpub(zpub) { - let data = b58.decode(zpub); - data = data.slice(4); - data = Buffer.concat([Buffer.from('0488b21e', 'hex'), data]); - - return b58.encode(data); - } - - static _getTransactionsFromHistories(histories) { - let txs = []; - for (let history of Object.values(histories)) { - for (let tx of history) { - txs.push(tx); - } - } - return txs; - } - - /** - * Broadcast txhex. Can throw an exception if failed - * - * @param {String} txhex - * @returns {Promise} - */ - async broadcastTx(txhex) { - let broadcast = await BlueElectrum.broadcastV2(txhex); - console.log({ broadcast }); - if (broadcast.indexOf('successfully') !== -1) return true; - return broadcast.length === 64; // this means return string is txid (precise length), so it was broadcasted ok - } } diff --git a/class/hd-segwit-p2sh-wallet.js b/class/hd-segwit-p2sh-wallet.js index 0205c1d3..32f8e3b6 100644 --- a/class/hd-segwit-p2sh-wallet.js +++ b/class/hd-segwit-p2sh-wallet.js @@ -1,48 +1,18 @@ -import { AbstractHDWallet } from './abstract-hd-wallet'; -import Frisbee from 'frisbee'; -import { NativeModules } from 'react-native'; import bip39 from 'bip39'; import BigNumber from 'bignumber.js'; import b58 from 'bs58check'; import signer from '../models/signer'; import { BitcoinUnit } from '../models/bitcoinUnits'; +import { AbstractHDElectrumWallet } from './abstract-hd-electrum-wallet'; const bitcoin = require('bitcoinjs-lib'); const HDNode = require('bip32'); -const { RNRandomBytes } = NativeModules; - -/** - * Converts ypub to xpub - * @param {String} ypub - wallet ypub - * @returns {*} - */ -function ypubToXpub(ypub) { - let data = b58.decode(ypub); - if (data.readUInt32BE() !== 0x049d7cb2) throw new Error('Not a valid ypub extended key!'); - data = data.slice(4); - data = Buffer.concat([Buffer.from('0488b21e', 'hex'), data]); - - return b58.encode(data); -} - -/** - * Creates Segwit P2SH Bitcoin address - * @param hdNode - * @returns {String} - */ -function nodeToP2shSegwitAddress(hdNode) { - const { address } = bitcoin.payments.p2sh({ - redeem: bitcoin.payments.p2wpkh({ pubkey: hdNode.publicKey }), - }); - return address; -} - /** * HD Wallet (BIP39). * In particular, BIP49 (P2SH Segwit) * @see https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki */ -export class HDSegwitP2SHWallet extends AbstractHDWallet { +export class HDSegwitP2SHWallet extends AbstractHDElectrumWallet { static type = 'HDsegwitP2SH'; static typeReadable = 'HD SegWit (BIP49 P2SH)'; @@ -54,37 +24,6 @@ export class HDSegwitP2SHWallet extends AbstractHDWallet { return true; } - async generate() { - let that = this; - return new Promise(function(resolve) { - if (typeof RNRandomBytes === 'undefined') { - // CLI/CI environment - // crypto should be provided globally by test launcher - return crypto.randomBytes(32, (err, buf) => { // eslint-disable-line - if (err) throw err; - that.secret = bip39.entropyToMnemonic(buf.toString('hex')); - resolve(); - }); - } - - // RN environment - RNRandomBytes.randomBytes(32, (err, bytes) => { - if (err) throw new Error(err); - let b = Buffer.from(bytes, 'base64').toString('hex'); - that.secret = bip39.entropyToMnemonic(b); - resolve(); - }); - }); - } - - _getExternalWIFByIndex(index) { - return this._getWIFByIndex(false, index); - } - - _getInternalWIFByIndex(index) { - return this._getWIFByIndex(true, index); - } - /** * Get internal/external WIF by wallet index * @param {Boolean} internal @@ -107,11 +46,11 @@ export class HDSegwitP2SHWallet extends AbstractHDWallet { if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit if (!this._node0) { - const xpub = ypubToXpub(this.getXpub()); + const xpub = this.constructor._ypubToXpub(this.getXpub()); const hdNode = HDNode.fromBase58(xpub); this._node0 = hdNode.derive(0); } - const address = nodeToP2shSegwitAddress(this._node0.derive(index)); + const address = this.constructor._nodeToP2shSegwitAddress(this._node0.derive(index)); return (this.external_addresses_cache[index] = address); } @@ -121,11 +60,11 @@ export class HDSegwitP2SHWallet extends AbstractHDWallet { if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit if (!this._node1) { - const xpub = ypubToXpub(this.getXpub()); + const xpub = this.constructor._ypubToXpub(this.getXpub()); const hdNode = HDNode.fromBase58(xpub); this._node1 = hdNode.derive(1); } - const address = nodeToP2shSegwitAddress(this._node1.derive(index)); + const address = this.constructor._nodeToP2shSegwitAddress(this._node1.derive(index)); return (this.internal_addresses_cache[index] = address); } @@ -158,108 +97,6 @@ export class HDSegwitP2SHWallet extends AbstractHDWallet { return this._xpub; } - async _getTransactionsBatch(addresses) { - const api = new Frisbee({ baseURI: 'https://blockchain.info' }); - let transactions = []; - let offset = 0; - - while (1) { - let response = await api.get('/multiaddr?active=' + addresses + '&n=100&offset=' + offset); - - if (response && response.body) { - if (response.body.txs && response.body.txs.length === 0) { - break; - } - - this._lastTxFetch = +new Date(); - - // processing TXs and adding to internal memory - if (response.body.txs) { - for (let tx of response.body.txs) { - let value = 0; - - for (let input of tx.inputs) { - // ----- INPUTS - - if (input.prev_out && input.prev_out.addr && this.weOwnAddress(input.prev_out.addr)) { - // this is outgoing from us - value -= input.prev_out.value; - } - } - - for (let output of tx.out) { - // ----- OUTPUTS - - if (output.addr && this.weOwnAddress(output.addr)) { - // this is incoming to us - value += output.value; - } - } - - tx.value = value; // new BigNumber(value).div(100000000).toString() * 1; - if (response.body.hasOwnProperty('info')) { - if (response.body.info.latest_block.height && tx.block_height) { - tx.confirmations = response.body.info.latest_block.height - tx.block_height + 1; - } else { - tx.confirmations = 0; - } - } else { - tx.confirmations = 0; - } - transactions.push(tx); - } - - if (response.body.txs.length < 100) { - // this fetch yilded less than page size, thus requesting next batch makes no sense - break; - } - } else { - break; // error ? - } - } else { - throw new Error('Could not fetch transactions from API: ' + response.err); // breaks here - } - - offset += 100; - } - return transactions; - } - - /** - * @inheritDoc - */ - async fetchTransactions() { - try { - if (this.usedAddresses.length === 0) { - // just for any case, refresh balance (it refreshes internal `this.usedAddresses`) - await this.fetchBalance(); - } - - this.transactions = []; - - let addresses4batch = []; - for (let addr of this.usedAddresses) { - addresses4batch.push(addr); - if (addresses4batch.length >= 45) { - let addresses = addresses4batch.join('|'); - let transactions = await this._getTransactionsBatch(addresses); - this.transactions = this.transactions.concat(transactions); - addresses4batch = []; - } - } - // final batch - for (let c = 0; c <= this.gap_limit; c++) { - addresses4batch.push(this._getExternalAddressByIndex(this.next_free_address_index + c)); - addresses4batch.push(this._getInternalAddressByIndex(this.next_free_change_address_index + c)); - } - let addresses = addresses4batch.join('|'); - let transactions = await this._getTransactionsBatch(addresses); - this.transactions = this.transactions.concat(transactions); - } catch (err) { - console.warn(err); - } - } - /** * * @param utxos @@ -291,4 +128,30 @@ export class HDSegwitP2SHWallet extends AbstractHDWallet { this._getInternalAddressByIndex(this.next_free_change_address_index), ); } + + /** + * Converts ypub to xpub + * @param {String} ypub - wallet ypub + * @returns {*} + */ + static _ypubToXpub(ypub) { + let data = b58.decode(ypub); + if (data.readUInt32BE() !== 0x049d7cb2) throw new Error('Not a valid ypub extended key!'); + data = data.slice(4); + data = Buffer.concat([Buffer.from('0488b21e', 'hex'), data]); + + return b58.encode(data); + } + + /** + * Creates Segwit P2SH Bitcoin address + * @param hdNode + * @returns {String} + */ + static _nodeToP2shSegwitAddress(hdNode) { + const { address } = bitcoin.payments.p2sh({ + redeem: bitcoin.payments.p2wpkh({ pubkey: hdNode.publicKey }), + }); + return address; + } } diff --git a/tests/integration/HDWallet.test.js b/tests/integration/HDWallet.test.js index 582e50fb..e98c83a3 100644 --- a/tests/integration/HDWallet.test.js +++ b/tests/integration/HDWallet.test.js @@ -55,7 +55,7 @@ it('can create a Segwit HD (BIP49)', async function() { assert.ok(hd._lastTxFetch === 0); await hd.fetchTransactions(); assert.ok(hd._lastTxFetch > 0); - assert.strictEqual(hd.transactions.length, 4); + assert.strictEqual(hd.getTransactions().length, 4); assert.strictEqual('L4MqtwJm6hkbACLG4ho5DF8GhcXdLEbbvpJnbzA9abfD6RDpbr2m', hd._getExternalWIFByIndex(0)); assert.strictEqual( @@ -84,7 +84,7 @@ it('HD (BIP49) can work with a gap', async function() { // console.log('external', c, hd._getExternalAddressByIndex(c)); // } await hd.fetchTransactions(); - assert.ok(hd.transactions.length >= 3); + assert.ok(hd.getTransactions().length >= 3); }); it('Segwit HD (BIP49) can batch fetch many txs', async function() { @@ -93,7 +93,7 @@ it('Segwit HD (BIP49) can batch fetch many txs', async function() { hd._xpub = 'ypub6WZ2c7YJ1SQ1rBYftwMqwV9bBmybXzETFxWmkzMz25bCf6FkDdXjNgR7zRW8JGSnoddNdUH7ZQS7JeQAddxdGpwgPskcsXFcvSn1JdGVcPQ'; await hd.fetchBalance(); await hd.fetchTransactions(); - assert.ok(hd.getTransactions().length === 153); + assert.strictEqual(hd.getTransactions().length, 152); }); it('Segwit HD (BIP49) can fetch more data if pointers to last_used_addr are lagging behind', async function() { @@ -104,7 +104,7 @@ it('Segwit HD (BIP49) can fetch more data if pointers to last_used_addr are lagg hd.next_free_address_index = 50; await hd.fetchBalance(); await hd.fetchTransactions(); - assert.strictEqual(hd.getTransactions().length, 153); + assert.strictEqual(hd.getTransactions().length, 152); }); it('Segwit HD (BIP49) can generate addressess only via ypub', function() { @@ -143,9 +143,14 @@ it('HD (BIP49) can create TX', async () => { hd.setSecret(process.env.HD_MNEMONIC_BIP49); assert.ok(hd.validateMnemonic()); + await hd.fetchBalance(); await hd.fetchUtxo(); - await hd.getChangeAddressAsync(); // to refresh internal pointer to next free address - await hd.getAddressAsync(); // to refresh internal pointer to next free address + assert.ok(typeof hd.utxo[0].confirmations === 'number'); + assert.ok(hd.utxo[0].txid); + assert.ok(hd.utxo[0].vout !== undefined); + assert.ok(hd.utxo[0].amount); + assert.ok(hd.utxo[0].address); + assert.ok(hd.utxo[0].wif); let txhex = hd.createTx(hd.utxo, 0.000014, 0.000001, '3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK'); assert.strictEqual( txhex, @@ -209,21 +214,6 @@ it('HD (BIP49) can create TX', async () => { assert.strictEqual(tx.outs[0].value, 75000); }); -it('Segwit HD (BIP49) can fetch UTXO', async function() { - let hd = new HDSegwitP2SHWallet(); - hd.usedAddresses = ['1Ez69SnzzmePmZX3WpEzMKTrcBF2gpNQ55', '1BiTCHeYzJNMxBLFCMkwYXNdFEdPJP53ZV']; // hacking internals - await hd.fetchUtxo(); - assert.ok(hd.utxo.length >= 12); - assert.ok(typeof hd.utxo[0].confirmations === 'number'); - assert.ok(hd.utxo[0].txid); - assert.ok(hd.utxo[0].vout); - assert.ok(hd.utxo[0].amount); - assert.ok( - hd.utxo[0].address && - (hd.utxo[0].address === '1Ez69SnzzmePmZX3WpEzMKTrcBF2gpNQ55' || hd.utxo[0].address === '1BiTCHeYzJNMxBLFCMkwYXNdFEdPJP53ZV'), - ); -}); - it('Segwit HD (BIP49) can fetch balance with many used addresses in hierarchy', async function() { if (!process.env.HD_MNEMONIC_BIP49_MANY_TX) { console.error('process.env.HD_MNEMONIC_BIP49_MANY_TX not set, skipped'); @@ -302,7 +292,7 @@ it('can create a Legacy HD (BIP44)', async function() { assert.ok(hd._lastTxFetch === 0); await hd.fetchTransactions(); assert.ok(hd._lastTxFetch > 0); - assert.strictEqual(hd.transactions.length, 4); + assert.strictEqual(hd.getTransactions().length, 4); assert.strictEqual(hd.next_free_address_index, 1); assert.strictEqual(hd.next_free_change_address_index, 1); @@ -403,7 +393,7 @@ it.skip('HD breadwallet works', async function() { assert.ok(hdBread._lastTxFetch === 0); await hdBread.fetchTransactions(); assert.ok(hdBread._lastTxFetch > 0); - assert.strictEqual(hdBread.transactions.length, 177); + assert.strictEqual(hdBread.getTransactions().length, 177); for (let tx of hdBread.getTransactions()) { assert.ok(tx.confirmations); } From 89e151898482c974c345a7dbc0d6d77142dac392 Mon Sep 17 00:00:00 2001 From: Overtorment Date: Tue, 24 Dec 2019 01:09:31 +0000 Subject: [PATCH 07/26] TST: simplify --- tests/integration/HDWallet.test.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/integration/HDWallet.test.js b/tests/integration/HDWallet.test.js index e98c83a3..cc230452 100644 --- a/tests/integration/HDWallet.test.js +++ b/tests/integration/HDWallet.test.js @@ -87,15 +87,6 @@ it('HD (BIP49) can work with a gap', async function() { assert.ok(hd.getTransactions().length >= 3); }); -it('Segwit HD (BIP49) can batch fetch many txs', async function() { - jasmine.DEFAULT_TIMEOUT_INTERVAL = 300 * 1000; - let hd = new HDSegwitP2SHWallet(); - hd._xpub = 'ypub6WZ2c7YJ1SQ1rBYftwMqwV9bBmybXzETFxWmkzMz25bCf6FkDdXjNgR7zRW8JGSnoddNdUH7ZQS7JeQAddxdGpwgPskcsXFcvSn1JdGVcPQ'; - await hd.fetchBalance(); - await hd.fetchTransactions(); - assert.strictEqual(hd.getTransactions().length, 152); -}); - it('Segwit HD (BIP49) can fetch more data if pointers to last_used_addr are lagging behind', async function() { jasmine.DEFAULT_TIMEOUT_INTERVAL = 300 * 1000; let hd = new HDSegwitP2SHWallet(); From 6dac7340efb3fabc4ca0297ad75b0de4a908935c Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Date: Sat, 28 Dec 2019 10:22:43 -0600 Subject: [PATCH 08/26] FIX: TX Time visual glitch --- BlueComponents.js | 10 ++++------ screen/wallets/list.js | 15 +++++++++++++-- screen/wallets/transactions.js | 13 ++++++++++++- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/BlueComponents.js b/BlueComponents.js index 5355b6c4..8cb31b11 100644 --- a/BlueComponents.js +++ b/BlueComponents.js @@ -1422,19 +1422,17 @@ export class NewWalletPanel extends Component { } } -export const BlueTransactionListItem = ({ item, itemPriceUnit = BitcoinUnit.BTC }) => { +export const BlueTransactionListItem = ({ item, itemPriceUnit = BitcoinUnit.BTC, shouldRefresh }) => { + const [transactionTimeToReadable, setTransactionTimeToReadable] = useState('...'); + const [subtitleNumberOfLines, setSubtitleNumberOfLines] = useState(1); const calculateTimeLabel = () => { const transactionTimeToReadable = loc.transactionTimeToReadable(item.received); return setTransactionTimeToReadable(transactionTimeToReadable); }; - const interval = setInterval(() => calculateTimeLabel(), 60000); - const [transactionTimeToReadable, setTransactionTimeToReadable] = useState('...'); - const [subtitleNumberOfLines, setSubtitleNumberOfLines] = useState(1); useEffect(() => { calculateTimeLabel(); - return () => clearInterval(interval); - }, [item, itemPriceUnit]); + }, [item, itemPriceUnit, shouldRefresh]); const txMemo = () => { if (BlueApp.tx_metadata[item.hash] && BlueApp.tx_metadata[item.hash]['memo']) { diff --git a/screen/wallets/list.js b/screen/wallets/list.js index 2c400cfd..d5b96546 100644 --- a/screen/wallets/list.js +++ b/screen/wallets/list.js @@ -41,8 +41,8 @@ export default class WalletsList extends Component { isFlatListRefreshControlHidden: true, wallets: BlueApp.getWallets().concat(false), lastSnappedTo: 0, + timeElpased: 0, }; - EV(EV.enum.WALLETS_COUNT_CHANGED, () => this.redrawScreen(true)); // here, when we receive TRANSACTIONS_COUNT_CHANGED we do not query @@ -72,6 +72,13 @@ export default class WalletsList extends Component { } if (noErr) this.redrawScreen(); }); + this.interval = setInterval(() => { + this.setState(prev => ({ timeElapsed: prev.timeElapsed + 1 })); + }, 60000); + } + + componentWillUnmount() { + clearInterval(this.interval); } /** @@ -308,7 +315,11 @@ export default class WalletsList extends Component { /> this.refreshTransactions()} refreshing={!this.state.isFlatListRefreshControlHidden} /> + this.refreshTransactions()} + refreshing={!this.state.isFlatListRefreshControlHidden} + shouldRefresh={this.state.timeElpased} + /> } > { + this.setState(prev => ({ timeElapsed: prev.timeElapsed + 1 })); + }, 60000); } /** @@ -402,10 +406,17 @@ export default class WalletTransactions extends Component { componentWillUnmount() { this.onWillBlur(); + clearInterval(this.interval); } renderItem = item => { - return ; + return ( + + ); }; render() { From adf00f1da014c04b148c8e262e69fb908d48cd7d Mon Sep 17 00:00:00 2001 From: Overtorment Date: Sat, 28 Dec 2019 18:24:53 +0000 Subject: [PATCH 09/26] TST: fix selftest --- screen/selftest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/screen/selftest.js b/screen/selftest.js index aa34ac8e..c49fd510 100644 --- a/screen/selftest.js +++ b/screen/selftest.js @@ -262,7 +262,7 @@ export default class Selftest extends Component { await hd3.fetchBalance(); if (hd3.getBalance() !== 26000) throw new Error('Could not fetch HD balance'); await hd3.fetchTransactions(); - if (hd3.transactions.length !== 1) throw new Error('Could not fetch HD transactions'); + if (hd3.getTransactions().length !== 1) throw new Error('Could not fetch HD transactions'); // From 3678c376a62c062ac33d0f964f6a99fede4186fa Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Date: Sat, 28 Dec 2019 13:42:59 -0600 Subject: [PATCH 10/26] FIX: Set isLoading to false when biometrics unlock fails --- UnlockWith.js | 1 + screen/send/confirm.js | 1 + 2 files changed, 2 insertions(+) diff --git a/UnlockWith.js b/UnlockWith.js index ffcca7ca..3ffed8c8 100644 --- a/UnlockWith.js +++ b/UnlockWith.js @@ -34,6 +34,7 @@ export default class UnlockWith extends Component { } this.setState({ isAuthenticating: true }, async () => { if (await Biometric.unlockWithBiometrics()) { + this.setState({ isAuthenticating: false }); await BlueApp.startAndDecrypt(); return this.props.onSuccessfullyAuthenticated(); } diff --git a/screen/send/confirm.js b/screen/send/confirm.js index 3a22ff9f..f34b3049 100644 --- a/screen/send/confirm.js +++ b/screen/send/confirm.js @@ -52,6 +52,7 @@ export default class Confirm extends Component { if (this.isBiometricUseCapableAndEnabled) { if (!(await Biometric.unlockWithBiometrics())) { + this.setState({ isLoading: false }); return; } } From 4903eb2373a2630d0925a268be6f939b87fd08f1 Mon Sep 17 00:00:00 2001 From: Nuno Coelho Date: Sun, 29 Dec 2019 18:31:33 +0000 Subject: [PATCH 11/26] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 19a66f46..4f42255e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ![](https://img.shields.io/github/license/BlueWallet/BlueWallet.svg) Thin Bitcoin Wallet. -Built with React Native and BlockCypher API. +Built with React Native and Electrum. [![Appstore](https://bluewallet.io/img/app-store-badge.svg)](https://itunes.apple.com/us/app/bluewallet-bitcoin-wallet/id1376878040?l=ru&ls=1&mt=8) [![Playstore](https://bluewallet.io/img/play-store-badge.svg)](https://play.google.com/store/apps/details?id=io.bluewallet.bluewallet) From bae0c09304f29e25f979ef9828f82b128e94549e Mon Sep 17 00:00:00 2001 From: Umar Bolatov Date: Sun, 29 Dec 2019 11:59:10 -0800 Subject: [PATCH 12/26] Add "engines" field to package.json address #792 --- package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package.json b/package.json index d37a2b88..fc9678ab 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,10 @@ "react-test-renderer": "16.8.6", "rn-nodeify": "github:tradle/rn-nodeify" }, + "engines": { + "node": ">=10.16.0", + "npm": ">=6.9.0" + }, "scripts": { "prepare": "./patches/fix_mangle.sh; git apply patches/minifier.js.patch; git apply patches/minify.js.patch", "clean": "cd android/; ./gradlew clean; cd ..; rm -r -f /tmp/metro-cache/; rm -r -f node_modules/; npm cache clean --force; npm i; npm start -- --reset-cache", From 3dd50f89dc566ed502b8e78970d98fa876fb0296 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Date: Sun, 29 Dec 2019 12:44:52 -0600 Subject: [PATCH 13/26] FIX: Don't show wallet export warning if wallet was imported. --- class/walletImport.js | 1 + 1 file changed, 1 insertion(+) diff --git a/class/walletImport.js b/class/walletImport.js index 969506d5..0279404f 100644 --- a/class/walletImport.js +++ b/class/walletImport.js @@ -28,6 +28,7 @@ export default class WalletImport { alert(loc.wallets.import.success); ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false }); w.setLabel(loc.wallets.import.imported + ' ' + w.typeReadable); + w.setUserHasSavedExport(true); WalletImport.removePlaceholderWallet(); BlueApp.wallets.push(w); await BlueApp.saveToDisk(); From 951034f87f6c7c1a43cdfcfb8249323a058dd344 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Rodriguez=20V=C3=A9lez?= Date: Mon, 30 Dec 2019 21:30:11 -0600 Subject: [PATCH 14/26] ADD: Lock App to Portrait mode --- android/app/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index da76e9d4..04c6060e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -18,6 +18,7 @@ android:label="@string/app_name" android:launchMode="singleInstance" android:configChanges="keyboard|keyboardHidden|orientation|screenSize" + android:screenOrientation="portrait" android:windowSoftInputMode="adjustResize"> From 847bfef406e3650f45d6944092196450fc398bba Mon Sep 17 00:00:00 2001 From: Umar Bolatov Date: Mon, 30 Dec 2019 21:51:24 -0800 Subject: [PATCH 15/26] Update Android build instructions address #759 --- README.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4f42255e..57713590 100644 --- a/README.md +++ b/README.md @@ -40,10 +40,23 @@ npm install * To run on Android: +You will now need to either connect an Android device to your computer or run an emulated Android device using AVD Manager which comes shipped with Android Studio. To run an emulator using AVD Manager: + +1. Download and run Android Studio +2. Click on "Open an existing Android Studio Project" +3. Open `build.gradle` file under `BlueWallet/android/` folder +4. Android Studio will take some time to set things up. Once everything is set up, go to `Tools` -> `AVD Manager` +5. Click on "Create Virtual Device..." and go through the steps to create a virtual device +6. Launch your newly created virtual device by clicking the `Play` button under `Actions` column + +Once you connected an Android device or launched an emulator, run this: + ``` -npm start android +npx react-native run-android ``` +The above command will build the app and install it. Once you launch the app it will take some time for all of the dependencies to load. Once everything loads up, you should have the built app running. + * To run on iOS: ``` From 16ecd3096b31c1876394b87efcb176b63b57e656 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Date: Fri, 27 Dec 2019 18:53:34 -0600 Subject: [PATCH 16/26] ADD: Swipe to Scan --- App.js | 252 +++-------------------------------- BlueComponents.js | 5 +- MainBottomTabs.js | 5 + class/deeplinkSchemaMatch.js | 224 +++++++++++++++++++++++++++++++ ios/Podfile.lock | 8 +- package-lock.json | 56 ++++++-- package.json | 1 + screen/lnd/scanLndInvoice.js | 13 +- screen/send/details.js | 10 +- screen/send/scanQrAddress.js | 120 +++++++++-------- screen/wallets/list.js | 210 +++++++++++++++++++---------- 11 files changed, 524 insertions(+), 380 deletions(-) create mode 100644 class/deeplinkSchemaMatch.js diff --git a/App.js b/App.js index 4530804c..7777220b 100644 --- a/App.js +++ b/App.js @@ -1,18 +1,17 @@ import React from 'react'; import { Linking, DeviceEventEmitter, AppState, Clipboard, StyleSheet, KeyboardAvoidingView, Platform, View } from 'react-native'; -import AsyncStorage from '@react-native-community/async-storage'; import Modal from 'react-native-modal'; import { NavigationActions } from 'react-navigation'; import MainBottomTabs from './MainBottomTabs'; import NavigationService from './NavigationService'; import { BlueTextCentered, BlueButton } from './BlueComponents'; import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; -import url from 'url'; -import { AppStorage, LightningCustodianWallet } from './class'; import { Chain } from './models/bitcoinUnits'; import QuickActions from 'react-native-quick-actions'; import * as Sentry from '@sentry/react-native'; import OnAppLaunch from './class/onAppLaunch'; +import DeeplinkSchemaMatch from './class/deeplinkSchemaMatch'; +import BitcoinBIP70TransactionDecode from './bip70/bip70'; const A = require('./analytics'); if (process.env.NODE_ENV !== 'development') { @@ -21,11 +20,9 @@ if (process.env.NODE_ENV !== 'development') { }); } -const bitcoin = require('bitcoinjs-lib'); const bitcoinModalString = 'Bitcoin address'; const lightningModalString = 'Lightning Invoice'; const loc = require('./loc'); -/** @type {AppStorage} */ const BlueApp = require('./BlueApp'); export default class App extends React.Component { @@ -62,7 +59,7 @@ export default class App extends React.Component { } else { const url = await Linking.getInitialURL(); if (url) { - if (this.hasSchema(url)) { + if (DeeplinkSchemaMatch.hasSchema(url)) { this.handleOpenURL({ url }); } } else { @@ -116,12 +113,23 @@ export default class App extends React.Component { const isAddressFromStoredWallet = BlueApp.getWallets().some(wallet => wallet.chain === Chain.ONCHAIN ? wallet.weOwnAddress(clipboard) : wallet.isInvoiceGeneratedByWallet(clipboard), ); + const isBitcoinAddress = + DeeplinkSchemaMatch.isBitcoinAddress(clipboard) || BitcoinBIP70TransactionDecode.matchesPaymentURL(clipboard); + const isLightningInvoice = DeeplinkSchemaMatch.isLightningInvoice(clipboard); + const isLNURL = DeeplinkSchemaMatch.isLnUrl(clipboard); + const isBothBitcoinAndLightning = DeeplinkSchemaMatch.isBothBitcoinAndLightning(clipboard); if ( - (!isAddressFromStoredWallet && - this.state.clipboardContent !== clipboard && - (this.isBitcoinAddress(clipboard) || this.isLightningInvoice(clipboard) || this.isLnUrl(clipboard))) || - this.isBothBitcoinAndLightning(clipboard) + !isAddressFromStoredWallet && + this.state.clipboardContent !== clipboard && + (isBitcoinAddress || isLightningInvoice || isLNURL || isBothBitcoinAndLightning) ) { + if (isBitcoinAddress) { + this.setState({ clipboardContentModalAddressType: bitcoinModalString }); + } else if (isLightningInvoice || isLNURL) { + this.setState({ clipboardContentModalAddressType: lightningModalString }); + } else if (isBothBitcoinAndLightning) { + this.setState({ clipboardContentModalAddressType: bitcoinModalString }); + } this.setState({ isClipboardContentModalVisible: true }); } this.setState({ clipboardContent: clipboard }); @@ -130,94 +138,6 @@ export default class App extends React.Component { } }; - hasSchema(schemaString) { - if (typeof schemaString !== 'string' || schemaString.length <= 0) return false; - const lowercaseString = schemaString.trim().toLowerCase(); - return ( - lowercaseString.startsWith('bitcoin:') || - lowercaseString.startsWith('lightning:') || - lowercaseString.startsWith('blue:') || - lowercaseString.startsWith('bluewallet:') || - lowercaseString.startsWith('lapp:') - ); - } - - isBitcoinAddress(address) { - address = address - .replace('bitcoin:', '') - .replace('bitcoin=', '') - .split('?')[0]; - let isValidBitcoinAddress = false; - try { - bitcoin.address.toOutputScript(address); - isValidBitcoinAddress = true; - this.setState({ clipboardContentModalAddressType: bitcoinModalString }); - } catch (err) { - isValidBitcoinAddress = false; - } - return isValidBitcoinAddress; - } - - isLightningInvoice(invoice) { - let isValidLightningInvoice = false; - if (invoice.toLowerCase().startsWith('lightning:lnb') || invoice.toLowerCase().startsWith('lnb')) { - this.setState({ clipboardContentModalAddressType: lightningModalString }); - isValidLightningInvoice = true; - } - return isValidLightningInvoice; - } - - isLnUrl(text) { - if (text.toLowerCase().startsWith('lightning:lnurl') || text.toLowerCase().startsWith('lnurl')) { - return true; - } - return false; - } - - isBothBitcoinAndLightning(url) { - if (url.includes('lightning') && url.includes('bitcoin')) { - const txInfo = url.split(/(bitcoin:|lightning:|lightning=|bitcoin=)+/); - let bitcoin; - let lndInvoice; - for (const [index, value] of txInfo.entries()) { - try { - // Inside try-catch. We dont wan't to crash in case of an out-of-bounds error. - if (value.startsWith('bitcoin')) { - bitcoin = `bitcoin:${txInfo[index + 1]}`; - if (!this.isBitcoinAddress(bitcoin)) { - bitcoin = false; - break; - } - } else if (value.startsWith('lightning')) { - lndInvoice = `lightning:${txInfo[index + 1]}`; - if (!this.isLightningInvoice(lndInvoice)) { - lndInvoice = false; - break; - } - } - } catch (e) { - console.log(e); - } - if (bitcoin && lndInvoice) break; - } - if (bitcoin && lndInvoice) { - this.setState({ - clipboardContent: { bitcoin, lndInvoice }, - }); - return { bitcoin, lndInvoice }; - } else { - return undefined; - } - } - return undefined; - } - - isSafelloRedirect(event) { - let urlObject = url.parse(event.url, true) // eslint-disable-line - - return !!urlObject.query['safello-state-token']; - } - isBothBitcoinAndLightningWalletSelect = wallet => { const clipboardContent = this.state.clipboardContent; if (wallet.chain === Chain.ONCHAIN) { @@ -246,141 +166,7 @@ export default class App extends React.Component { }; handleOpenURL = event => { - if (event.url === null) { - return; - } - if (typeof event.url !== 'string') { - return; - } - let isBothBitcoinAndLightning; - try { - isBothBitcoinAndLightning = this.isBothBitcoinAndLightning(event.url); - } catch (e) { - console.log(e); - } - if (isBothBitcoinAndLightning) { - this.navigator && - this.navigator.dispatch( - NavigationActions.navigate({ - routeName: 'HandleOffchainAndOnChain', - params: { - onWalletSelect: this.isBothBitcoinAndLightningWalletSelect, - }, - }), - ); - } else if (this.isBitcoinAddress(event.url)) { - this.navigator && - this.navigator.dispatch( - NavigationActions.navigate({ - routeName: 'SendDetails', - params: { - uri: event.url, - }, - }), - ); - } else if (this.isLightningInvoice(event.url)) { - this.navigator && - this.navigator.dispatch( - NavigationActions.navigate({ - routeName: 'ScanLndInvoice', - params: { - uri: event.url, - }, - }), - ); - } else if (this.isLnUrl(event.url)) { - this.navigator && - this.navigator.dispatch( - NavigationActions.navigate({ - routeName: 'LNDCreateInvoice', - params: { - uri: event.url, - }, - }), - ); - } else if (this.isSafelloRedirect(event)) { - let urlObject = url.parse(event.url, true) // eslint-disable-line - - const safelloStateToken = urlObject.query['safello-state-token']; - - this.navigator && - this.navigator.dispatch( - NavigationActions.navigate({ - routeName: 'BuyBitcoin', - params: { - uri: event.url, - safelloStateToken, - }, - }), - ); - } else { - let urlObject = url.parse(event.url, true); // eslint-disable-line - console.log('parsed', urlObject); - (async () => { - if (urlObject.protocol === 'bluewallet:' || urlObject.protocol === 'lapp:' || urlObject.protocol === 'blue:') { - switch (urlObject.host) { - case 'openlappbrowser': - console.log('opening LAPP', urlObject.query.url); - // searching for LN wallet: - let haveLnWallet = false; - for (let w of BlueApp.getWallets()) { - if (w.type === LightningCustodianWallet.type) { - haveLnWallet = true; - } - } - - if (!haveLnWallet) { - // need to create one - let w = new LightningCustodianWallet(); - w.setLabel(this.state.label || w.typeReadable); - - try { - let lndhub = await AsyncStorage.getItem(AppStorage.LNDHUB); - if (lndhub) { - w.setBaseURI(lndhub); - w.init(); - } - await w.createAccount(); - await w.authorize(); - } catch (Err) { - // giving up, not doing anything - return; - } - BlueApp.wallets.push(w); - await BlueApp.saveToDisk(); - } - - // now, opening lapp browser and navigating it to URL. - // looking for a LN wallet: - let lnWallet; - for (let w of BlueApp.getWallets()) { - if (w.type === LightningCustodianWallet.type) { - lnWallet = w; - break; - } - } - - if (!lnWallet) { - // something went wrong - return; - } - - this.navigator && - this.navigator.dispatch( - NavigationActions.navigate({ - routeName: 'LappBrowser', - params: { - fromSecret: lnWallet.getSecret(), - fromWallet: lnWallet, - url: urlObject.query.url, - }, - }), - ); - break; - } - } - })(); - } + DeeplinkSchemaMatch.navigationRouteFor(event, value => this.navigator && this.navigator.dispatch(NavigationActions.navigate(value))); }; renderClipboardContentModal = () => { diff --git a/BlueComponents.js b/BlueComponents.js index 8cb31b11..169fa783 100644 --- a/BlueComponents.js +++ b/BlueComponents.js @@ -2037,7 +2037,8 @@ export class BlueAddressInput extends Component { static propTypes = { isLoading: PropTypes.bool, onChangeText: PropTypes.func, - onBarScanned: PropTypes.func, + onBarScanned: PropTypes.func.isRequired, + launchedBy: PropTypes.string.isRequired, address: PropTypes.string, placeholder: PropTypes.string, }; @@ -2081,7 +2082,7 @@ export class BlueAddressInput extends Component { { - NavigationService.navigate('ScanQrAddress', { onBarScanned: this.props.onBarScanned }); + NavigationService.navigate('ScanQrAddress', { onBarScanned: this.props.onBarScanned, launchedBy: this.props.launchedBy }); Keyboard.dismiss(); }} style={{ diff --git a/MainBottomTabs.js b/MainBottomTabs.js index aadd42e6..2626176d 100644 --- a/MainBottomTabs.js +++ b/MainBottomTabs.js @@ -60,6 +60,9 @@ const WalletsStackNavigator = createStackNavigator( Wallets: { screen: WalletsList, path: 'wallets', + navigationOptions: { + header: null, + }, }, WalletTransactions: { screen: WalletTransactions, @@ -157,6 +160,7 @@ const WalletsStackNavigator = createStackNavigator( const CreateTransactionStackNavigator = createStackNavigator({ SendDetails: { + routeName: 'SendDetails', screen: sendDetails, }, Confirm: { @@ -290,6 +294,7 @@ const MainBottomTabs = createStackNavigator( }, // SendDetails: { + routeName: 'SendDetails', screen: CreateTransactionStackNavigator, navigationOptions: { header: null, diff --git a/class/deeplinkSchemaMatch.js b/class/deeplinkSchemaMatch.js new file mode 100644 index 00000000..cf95597f --- /dev/null +++ b/class/deeplinkSchemaMatch.js @@ -0,0 +1,224 @@ +import { AppStorage, LightningCustodianWallet } from './'; +import AsyncStorage from '@react-native-community/async-storage'; +import BitcoinBIP70TransactionDecode from '../bip70/bip70'; +const bitcoin = require('bitcoinjs-lib'); +const BlueApp = require('../BlueApp'); +class DeeplinkSchemaMatch { + static hasSchema(schemaString) { + if (typeof schemaString !== 'string' || schemaString.length <= 0) return false; + const lowercaseString = schemaString.trim().toLowerCase(); + return ( + lowercaseString.startsWith('bitcoin:') || + lowercaseString.startsWith('lightning:') || + lowercaseString.startsWith('blue:') || + lowercaseString.startsWith('bluewallet:') || + lowercaseString.startsWith('lapp:') + ); + } + + /** + * Examines the content of the event parameter. + * If the content is recognizable, create a dictionary with the respective + * navigation dictionary required by react-navigation + * @param {Object} event + * @param {void} completionHandler + */ + static navigationRouteFor(event, completionHandler) { + if (event.url === null) { + return; + } + if (typeof event.url !== 'string') { + return; + } + let isBothBitcoinAndLightning; + try { + isBothBitcoinAndLightning = DeeplinkSchemaMatch.isBothBitcoinAndLightning(event.url); + } catch (e) { + console.log(e); + } + if (isBothBitcoinAndLightning) { + completionHandler({ + routeName: 'HandleOffchainAndOnChain', + params: { + onWalletSelect: this.isBothBitcoinAndLightningWalletSelect, + }, + }); + } else if (DeeplinkSchemaMatch.isBitcoinAddress(event.url) || BitcoinBIP70TransactionDecode.matchesPaymentURL(event.url)) { + completionHandler({ + routeName: 'SendDetails', + params: { + uri: event.url, + }, + }); + } else if (DeeplinkSchemaMatch.isLightningInvoice(event.url)) { + completionHandler({ + routeName: 'ScanLndInvoice', + params: { + uri: event.url, + }, + }); + } else if (DeeplinkSchemaMatch.isLnUrl(event.url)) { + completionHandler({ + routeName: 'LNDCreateInvoice', + params: { + uri: event.url, + }, + }); + } else if (DeeplinkSchemaMatch.isSafelloRedirect(event)) { + let urlObject = url.parse(event.url, true) // eslint-disable-line + + const safelloStateToken = urlObject.query['safello-state-token']; + + completionHandler({ + routeName: 'BuyBitcoin', + params: { + uri: event.url, + safelloStateToken, + }, + }); + } else { + let urlObject = url.parse(event.url, true); // eslint-disable-line + console.log('parsed', urlObject); + (async () => { + if (urlObject.protocol === 'bluewallet:' || urlObject.protocol === 'lapp:' || urlObject.protocol === 'blue:') { + switch (urlObject.host) { + case 'openlappbrowser': + console.log('opening LAPP', urlObject.query.url); + // searching for LN wallet: + let haveLnWallet = false; + for (let w of BlueApp.getWallets()) { + if (w.type === LightningCustodianWallet.type) { + haveLnWallet = true; + } + } + + if (!haveLnWallet) { + // need to create one + let w = new LightningCustodianWallet(); + w.setLabel(this.state.label || w.typeReadable); + + try { + let lndhub = await AsyncStorage.getItem(AppStorage.LNDHUB); + if (lndhub) { + w.setBaseURI(lndhub); + w.init(); + } + await w.createAccount(); + await w.authorize(); + } catch (Err) { + // giving up, not doing anything + return; + } + BlueApp.wallets.push(w); + await BlueApp.saveToDisk(); + } + + // now, opening lapp browser and navigating it to URL. + // looking for a LN wallet: + let lnWallet; + for (let w of BlueApp.getWallets()) { + if (w.type === LightningCustodianWallet.type) { + lnWallet = w; + break; + } + } + + if (!lnWallet) { + // something went wrong + return; + } + + this.navigator && + this.navigator.dispatch( + completionHandler({ + routeName: 'LappBrowser', + params: { + fromSecret: lnWallet.getSecret(), + fromWallet: lnWallet, + url: urlObject.query.url, + }, + }), + ); + break; + } + } + })(); + } + } + + static isBitcoinAddress(address) { + address = address + .replace('bitcoin:', '') + .replace('bitcoin=', '') + .split('?')[0]; + let isValidBitcoinAddress = false; + try { + bitcoin.address.toOutputScript(address); + isValidBitcoinAddress = true; + } catch (err) { + isValidBitcoinAddress = false; + } + return isValidBitcoinAddress; + } + + static isLightningInvoice(invoice) { + let isValidLightningInvoice = false; + if (invoice.toLowerCase().startsWith('lightning:lnb') || invoice.toLowerCase().startsWith('lnb')) { + isValidLightningInvoice = true; + } + return isValidLightningInvoice; + } + + static isLnUrl(text) { + if (text.toLowerCase().startsWith('lightning:lnurl') || text.toLowerCase().startsWith('lnurl')) { + return true; + } + return false; + } + + static isSafelloRedirect(event) { + let urlObject = url.parse(event.url, true) // eslint-disable-line + + return !!urlObject.query['safello-state-token']; + } + + static isBothBitcoinAndLightning(url) { + if (url.includes('lightning') && url.includes('bitcoin')) { + const txInfo = url.split(/(bitcoin:|lightning:|lightning=|bitcoin=)+/); + let bitcoin; + let lndInvoice; + for (const [index, value] of txInfo.entries()) { + try { + // Inside try-catch. We dont wan't to crash in case of an out-of-bounds error. + if (value.startsWith('bitcoin')) { + bitcoin = `bitcoin:${txInfo[index + 1]}`; + if (!DeeplinkSchemaMatch.isBitcoinAddress(bitcoin)) { + bitcoin = false; + break; + } + } else if (value.startsWith('lightning')) { + lndInvoice = `lightning:${txInfo[index + 1]}`; + if (!this.isLightningInvoice(lndInvoice)) { + lndInvoice = false; + break; + } + } + } catch (e) { + console.log(e); + } + if (bitcoin && lndInvoice) break; + } + if (bitcoin && lndInvoice) { + this.setState({ + clipboardContent: { bitcoin, lndInvoice }, + }); + return { bitcoin, lndInvoice }; + } else { + return undefined; + } + } + return undefined; + } +} + +export default DeeplinkSchemaMatch; diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 6fd4e975..e48e7c09 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -142,9 +142,9 @@ PODS: - React - RNWatch (0.4.1): - React - - Sentry (4.4.2): - - Sentry/Core (= 4.4.2) - - Sentry/Core (4.4.2) + - Sentry (4.4.3): + - Sentry/Core (= 4.4.3) + - Sentry/Core (4.4.3) - swift_qrcodejs (1.1.2) - TcpSockets (3.3.2): - React @@ -362,7 +362,7 @@ SPEC CHECKSUMS: RNSVG: 0eb087cfb5d7937be93c45b163b26352a647e681 RNVectorIcons: 0bb4def82230be1333ddaeee9fcba45f0b288ed4 RNWatch: a14e378448e187cc12f307f61d41fe8a65400e86 - Sentry: bba998b0fb157fdd6596aa73290a9d67ae47be79 + Sentry: 14bdd673870e8cf64932b149fad5bbbf39a9b390 swift_qrcodejs: 4d024fc98b0778b804ec6a5c810880fd092aec9d TcpSockets: 8d839b9b14f6f344d98e4642ded13ab3112b462d ToolTipMenu: bdcaa0e888bcf44778a67fe34639b094352e904e diff --git a/package-lock.json b/package-lock.json index 1fc24ea3..8e37454d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5354,7 +5354,8 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -5372,11 +5373,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5389,15 +5392,18 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -5500,7 +5506,8 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -5510,6 +5517,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5522,17 +5530,20 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true + "bundled": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -5549,6 +5560,7 @@ "mkdirp": { "version": "0.5.1", "bundled": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -5621,7 +5633,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -5631,6 +5644,7 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { "wrappy": "1" } @@ -5706,7 +5720,8 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true + "bundled": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -5736,6 +5751,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5753,6 +5769,7 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5791,11 +5808,13 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "optional": true }, "yallist": { "version": "3.0.3", - "bundled": true + "bundled": true, + "optional": true } } }, @@ -5902,6 +5921,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", "dev": true, + "optional": true, "requires": { "is-glob": "^2.0.0" } @@ -6418,7 +6438,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", - "dev": true + "dev": true, + "optional": true }, "is-finite": { "version": "1.0.2", @@ -6450,6 +6471,7 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", "dev": true, + "optional": true, "requires": { "is-extglob": "^1.0.0" } @@ -11131,6 +11153,13 @@ "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-9.5.1.tgz", "integrity": "sha512-cRGfomzG/5LEwuJ6ct3m5yccckeI9aj8BNYwDPVxOeJ84LuJuvk5OqcjlYNeEzOWmWiH+QrFXfpLH1ag04bUeQ==" }, + "react-native-swiper": { + "version": "git+https://github.com/BlueWallet/react-native-swiper.git#e4dbde6657f6c66cba50f8abca7c472b47297c7f", + "from": "git+https://github.com/BlueWallet/react-native-swiper.git#1.5.14", + "requires": { + "prop-types": "^15.5.10" + } + }, "react-native-tab-view": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-1.4.1.tgz", @@ -11468,7 +11497,8 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "dev": true, + "optional": true }, "string_decoder": { "version": "1.1.1", diff --git a/package.json b/package.json index fc9678ab..40b86efb 100644 --- a/package.json +++ b/package.json @@ -116,6 +116,7 @@ "react-native-snap-carousel": "3.8.4", "react-native-sortable-list": "0.0.23", "react-native-svg": "9.5.1", + "react-native-swiper": "git+https://github.com/BlueWallet/react-native-swiper.git#1.5.14", "react-native-tcp": "git+https://github.com/aprock/react-native-tcp.git", "react-native-tooltip": "git+https://github.com/marcosrdz/react-native-tooltip.git", "react-native-vector-icons": "6.6.0", diff --git a/screen/lnd/scanLndInvoice.js b/screen/lnd/scanLndInvoice.js index d54a5eed..c10a3952 100644 --- a/screen/lnd/scanLndInvoice.js +++ b/screen/lnd/scanLndInvoice.js @@ -19,6 +19,7 @@ import { BlueNavigationStyle, BlueAddressInput, BlueBitcoinAmount, + BlueLoading, } from '../../BlueComponents'; import { LightningCustodianWallet } from '../../class/lightning-custodian-wallet'; import { BitcoinUnit, Chain } from '../../models/bitcoinUnits'; @@ -45,7 +46,8 @@ export default class ScanLndInvoice extends React.Component { constructor(props) { super(props); - + this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this._keyboardDidShow); + this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this._keyboardDidHide); if (!BlueApp.getWallets().some(item => item.type === LightningCustodianWallet.type)) { ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); alert('Before paying a Lightning invoice, you must first add a Lightning wallet.'); @@ -78,9 +80,7 @@ export default class ScanLndInvoice extends React.Component { } } - async componentDidMount() { - this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this._keyboardDidShow); - this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this._keyboardDidHide); + componentDidMount() { if (this.props.navigation.state.params.uri) { this.processTextForInvoice(this.props.navigation.getParam('uri')); } @@ -265,6 +265,9 @@ export default class ScanLndInvoice extends React.Component { }; render() { + if (!this.state.fromWallet) { + return ; + } return ( @@ -300,6 +303,7 @@ export default class ScanLndInvoice extends React.Component { isLoading={this.state.isLoading} placeholder={loc.lnd.placeholder} inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID} + launchedBy={this.props.navigation.state.routeName} /> {this.state.addresses.length > 1 && ( @@ -1067,6 +1070,7 @@ SendDetails.propTypes = { getParam: PropTypes.func, setParams: PropTypes.func, state: PropTypes.shape({ + routeName: PropTypes.string, params: PropTypes.shape({ amount: PropTypes.number, address: PropTypes.string, diff --git a/screen/send/scanQrAddress.js b/screen/send/scanQrAddress.js index b00ab06d..e67c4a2c 100644 --- a/screen/send/scanQrAddress.js +++ b/screen/send/scanQrAddress.js @@ -1,30 +1,42 @@ /* global alert */ -import React from 'react'; -import { Image, TouchableOpacity, Platform } from 'react-native'; -import PropTypes from 'prop-types'; +import React, { useEffect, useState } from 'react'; +import { Image, View, TouchableOpacity, Platform } from 'react-native'; import { RNCamera } from 'react-native-camera'; -import { SafeBlueArea } from '../../BlueComponents'; import { Icon } from 'react-native-elements'; import ImagePicker from 'react-native-image-picker'; +import PropTypes from 'prop-types'; +import { useNavigationParam, useNavigation } from 'react-navigation-hooks'; const LocalQRCode = require('@remobile/react-native-qrcode-local-image'); -export default class ScanQRCode extends React.Component { - static navigationOptions = { - header: null, - }; +const ScanQRCode = ({ + onBarScanned = useNavigationParam('onBarScanned'), + cameraPreviewIsPaused = false, + showCloseButton = true, + launchedBy = useNavigationParam('launchedBy'), +}) => { + const [isLoading, setIsLoading] = useState(false); + const { navigate } = useNavigation(); - cameraRef = null; + const onBarCodeRead = ret => { + if (!isLoading && !cameraPreviewIsPaused) { + setIsLoading(true); + try { + if (showCloseButton && launchedBy) { + navigate(launchedBy); + } + onBarScanned(ret.data); + } catch (e) { + console.log(e); + } + } + setIsLoading(false); + }; - onBarCodeRead = ret => { - if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.pausePreview(); - const onBarScannedProp = this.props.navigation.getParam('onBarScanned'); - this.props.navigation.goBack(); - onBarScannedProp(ret.data); - }; // end + useEffect(() => {}, [cameraPreviewIsPaused]); - render() { - return ( - + return ( + + {!cameraPreviewIsPaused && !isLoading && ( (this.cameraRef = ref)} - style={{ flex: 1, justifyContent: 'space-between' }} - onBarCodeRead={this.onBarCodeRead} + style={{ flex: 1, justifyContent: 'space-between', backgroundColor: '#000000' }} + onBarCodeRead={onBarCodeRead} barCodeTypes={[RNCamera.Constants.BarCodeType.qr]} /> + )} + {showCloseButton && ( this.props.navigation.goBack(null)} + onPress={() => navigate(launchedBy)} > - { - if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.pausePreview(); + )} + { + if (!isLoading) { + setIsLoading(true); ImagePicker.launchImageLibrary( { title: null, @@ -77,30 +92,31 @@ export default class ScanQRCode extends React.Component { const uri = Platform.OS === 'ios' ? response.uri.toString().replace('file://', '') : response.path.toString(); LocalQRCode.decode(uri, (error, result) => { if (!error) { - this.onBarCodeRead({ data: result }); + onBarCodeRead({ data: result }); } else { - if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.resumePreview(); alert('The selected image does not contain a QR Code.'); } }); - } else { - if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.resumePreview(); } + setIsLoading(false); }, ); - }} - > - - - - ); - } -} + } + }} + > + + + + ); +}; +ScanQRCode.navigationOptions = { + header: null, +}; ScanQRCode.propTypes = { - navigation: PropTypes.shape({ - goBack: PropTypes.func, - dismiss: PropTypes.func, - getParam: PropTypes.func, - }), + launchedBy: PropTypes.string, + onBarScanned: PropTypes.func, + cameraPreviewIsPaused: PropTypes.bool, + showCloseButton: PropTypes.bool, }; +export default ScanQRCode; diff --git a/screen/wallets/list.js b/screen/wallets/list.js index d5b96546..a52ff288 100644 --- a/screen/wallets/list.js +++ b/screen/wallets/list.js @@ -1,6 +1,17 @@ /* global alert */ import React, { Component } from 'react'; -import { View, TouchableOpacity, Text, FlatList, InteractionManager, RefreshControl, ScrollView, Alert } from 'react-native'; +import { + View, + StatusBar, + TouchableOpacity, + Text, + StyleSheet, + 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'; @@ -8,6 +19,9 @@ import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; import PropTypes from 'prop-types'; import { PlaceholderWallet } from '../../class'; import WalletImport from '../../class/walletImport'; +import Swiper from 'react-native-swiper'; +import ScanQRCode from '../send/scanQrAddress'; +import DeeplinkSchemaMatch from '../../class/deeplinkSchemaMatch'; let EV = require('../../events'); let A = require('../../analytics'); /** @type {AppStorage} */ @@ -16,23 +30,8 @@ let loc = require('../../loc'); let BlueElectrum = require('../../BlueElectrum'); export default class WalletsList extends Component { - static navigationOptions = ({ navigation }) => ({ - headerStyle: { - backgroundColor: '#FFFFFF', - borderBottomWidth: 0, - elevation: 0, - }, - headerRight: ( - navigation.navigate('Settings')} - > - - - ), - }); - walletsCarousel = React.createRef(); + swiperRef = React.createRef(); constructor(props) { super(props); @@ -299,21 +298,47 @@ export default class WalletsList extends Component { } }; + onSwiperIndexChanged = index => { + StatusBar.setBarStyle(index === 1 ? 'dark-content' : 'light-content'); + this.setState({ cameraPreviewIsPaused: index === 1 }); + }; + + onBarScanned = value => { + DeeplinkSchemaMatch.navigationRouteFor({ url: value }, completionValue => { + ReactNativeHapticFeedback.trigger('impactLight', { ignoreAndroidSystemSettings: false }); + this.props.navigation.navigate(completionValue); + }); + }; + _renderItem = data => { return ; }; + + renderNavigationHeader = () => { + return ( + + this.props.navigation.navigate('Settings')}> + + + + ); + }; + render() { if (this.state.isLoading) { return ; } return ( - + { + onDidFocus={() => { this.redrawScreen(); + this.setState({ cameraPreviewIsPaused: this.swiperRef.current.index === 1 }); }} + onWillBlur={() => this.setState({ cameraPreviewIsPaused: true })} /> this.refreshTransactions()} @@ -322,65 +347,112 @@ export default class WalletsList extends Component { /> } > - wallet.type === PlaceholderWallet.type) - ? () => this.props.navigation.navigate('AddWallet') - : null - } - /> - { - this.handleClick(index); - }} - handleLongPress={this.handleLongPress} - onSnapToItem={index => { - this.onSnapToItem(index); - }} - ref={c => (this.walletsCarousel = c)} - /> - - - + + + + + + {this.renderNavigationHeader()} + this.refreshTransactions()} refreshing={!this.state.isFlatListRefreshControlHidden} /> + } + > + wallet.type === PlaceholderWallet.type) + ? () => this.props.navigation.navigate('AddWallet') + : null + } + /> + { + this.handleClick(index); }} - > - {loc.wallets.list.empty_txs1} - - { + this.onSnapToItem(index); }} - > - {loc.wallets.list.empty_txs2} - - - } - data={this.state.dataSource} - extraData={this.state.dataSource} - keyExtractor={this._keyExtractor} - renderItem={this._renderItem} - /> - + ref={c => (this.walletsCarousel = c)} + /> + + + + {loc.wallets.list.empty_txs1} + + + {loc.wallets.list.empty_txs2} + + + } + data={this.state.dataSource} + extraData={this.state.dataSource} + keyExtractor={this._keyExtractor} + renderItem={this._renderItem} + /> + + + + + - + ); } } +const styles = StyleSheet.create({ + wrapper: { + backgroundColor: '#FFFFFF', + }, + walletsListWrapper: { + flex: 1, + backgroundColor: '#FFFFFF', + }, + scanQRWrapper: { + flex: 1, + backgroundColor: '#000000', + }, +}); + WalletsList.propTypes = { navigation: PropTypes.shape({ + state: PropTypes.shape({ + routeName: PropTypes.string, + }), navigate: PropTypes.func, }), }; From 1e4b655fa0319459dc457792654bc0d02abe6843 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Date: Tue, 31 Dec 2019 14:27:59 -0600 Subject: [PATCH 17/26] ADD: Tests for DeeplinkSchemaMatch --- class/deeplinkSchemaMatch.js | 3 -- tests/integration/deepLinkSchemaMatch.test.js | 50 +++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 tests/integration/deepLinkSchemaMatch.test.js diff --git a/class/deeplinkSchemaMatch.js b/class/deeplinkSchemaMatch.js index cf95597f..194b0c44 100644 --- a/class/deeplinkSchemaMatch.js +++ b/class/deeplinkSchemaMatch.js @@ -209,9 +209,6 @@ class DeeplinkSchemaMatch { if (bitcoin && lndInvoice) break; } if (bitcoin && lndInvoice) { - this.setState({ - clipboardContent: { bitcoin, lndInvoice }, - }); return { bitcoin, lndInvoice }; } else { return undefined; diff --git a/tests/integration/deepLinkSchemaMatch.test.js b/tests/integration/deepLinkSchemaMatch.test.js new file mode 100644 index 00000000..f852b56c --- /dev/null +++ b/tests/integration/deepLinkSchemaMatch.test.js @@ -0,0 +1,50 @@ +/* global describe, it, expect */ +import DeeplinkSchemaMatch from '../../class/deeplinkSchemaMatch'; +const assert = require('assert'); + +describe('unit - DeepLinkSchemaMatch', function() { + it('hasSchema', () => { + const hasSchema = DeeplinkSchemaMatch.hasSchema('bitcoin:12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG'); + assert.ok(hasSchema); + }); + + it('isBitcoin Address', () => { + assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG')); + }); + + it('isLighting Invoice', () => { + assert.ok( + DeeplinkSchemaMatch.isLightningInvoice( + 'lightning:lnbc10u1pwjqwkkpp5vlc3tttdzhpk9fwzkkue0sf2pumtza7qyw9vucxyyeh0yaqq66yqdq5f38z6mmwd3ujqar9wd6qcqzpgxq97zvuqrzjqvgptfurj3528snx6e3dtwepafxw5fpzdymw9pj20jj09sunnqmwqz9hx5qqtmgqqqqqqqlgqqqqqqgqjq5duu3fs9xq9vn89qk3ezwpygecu4p3n69wm3tnl28rpgn2gmk5hjaznemw0gy32wrslpn3g24khcgnpua9q04fttm2y8pnhmhhc2gncplz0zde', + ), + ); + }); + + it('isBoth Bitcoin & Invoice', () => { + assert.ok( + DeeplinkSchemaMatch.isBothBitcoinAndLightning( + 'bitcoin:1DamianM2k8WfNEeJmyqSe2YW1upB7UATx?amount=0.000001&lightning=lnbc1u1pwry044pp53xlmkghmzjzm3cljl6729cwwqz5hhnhevwfajpkln850n7clft4sdqlgfy4qv33ypmj7sj0f32rzvfqw3jhxaqcqzysxq97zvuq5zy8ge6q70prnvgwtade0g2k5h2r76ws7j2926xdjj2pjaq6q3r4awsxtm6k5prqcul73p3atveljkn6wxdkrcy69t6k5edhtc6q7lgpe4m5k4', + ), + ); + }); + + it('isLnurl', () => { + assert.ok( + DeeplinkSchemaMatch.isLnUrl( + 'LNURL1DP68GURN8GHJ7UM9WFMXJCM99E3K7MF0V9CXJ0M385EKVCENXC6R2C35XVUKXEFCV5MKVV34X5EKZD3EV56NYD3HXQURZEPEXEJXXEPNXSCRVWFNV9NXZCN9XQ6XYEFHVGCXXCMYXYMNSERXFQ5FNS', + ), + ); + }); + + it('navigationForRoute', () => { + const event = { uri: '12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG' }; + DeeplinkSchemaMatch.navigationRouteFor(event, navValue => { + assert.strictEqual(navValue, { + routeName: 'SendDetails', + params: { + uri: event.url, + }, + }); + }); + }); +}); From 24a32599d41aacb6a89eb868a4db6aa34fb565c8 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Date: Thu, 2 Jan 2020 00:09:14 -0600 Subject: [PATCH 18/26] OPS: Package updates --- ios/Podfile.lock | 24 +++---- package-lock.json | 163 ++++++++++++++++++++++++++-------------------- package.json | 10 +-- 3 files changed, 110 insertions(+), 87 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 6fd4e975..254adc46 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -82,7 +82,7 @@ PODS: - React - react-native-randombytes (3.5.3): - React - - react-native-slider (2.0.0-rc.1): + - react-native-slider (2.0.8): - React - react-native-webview (6.9.0): - React @@ -125,13 +125,13 @@ PODS: - React - RNHandoff (0.0.3): - React - - RNQuickAction (0.3.12): + - RNQuickAction (0.3.13): - React - RNRate (1.0.1): - React - RNSecureKeyStore (1.0.0): - React - - RNSentry (1.0.9): + - RNSentry (1.2.1): - React - Sentry (~> 4.4.0) - RNShare (2.0.0): @@ -140,11 +140,11 @@ PODS: - React - RNVectorIcons (6.6.0): - React - - RNWatch (0.4.1): + - RNWatch (0.4.2): - React - - Sentry (4.4.2): - - Sentry/Core (= 4.4.2) - - Sentry/Core (4.4.2) + - Sentry (4.4.3): + - Sentry/Core (= 4.4.3) + - Sentry/Core (4.4.3) - swift_qrcodejs (1.1.2) - TcpSockets (3.3.2): - React @@ -334,7 +334,7 @@ SPEC CHECKSUMS: react-native-haptic-feedback: 22c9dc85fd8059f83bf9edd9212ac4bd4ae6074d react-native-image-picker: 3637d63fef7e32a230141ab4660d3ceb773c824f react-native-randombytes: 991545e6eaaf700b4ee384c291ef3d572e0b2ca8 - react-native-slider: 6d83f7b8076a84e965a43fbdcfcf9dac19cea42e + react-native-slider: b2f361499888302147205f17f6fffa921a7bda70 react-native-webview: f72ac4078e115dfa741cc588acb1cca25566457d React-RCTActionSheet: b0f1ea83f4bf75fb966eae9bfc47b78c8d3efd90 React-RCTAnimation: 359ba1b5690b1e87cc173558a78e82d35919333e @@ -354,15 +354,15 @@ SPEC CHECKSUMS: RNFS: c9bbde46b0d59619f8e7b735991c60e0f73d22c1 RNGestureHandler: 5329a942fce3d41c68b84c2c2276ce06a696d8b0 RNHandoff: d3b0754cca3a6bcd9b25f544f733f7f033ccf5fa - RNQuickAction: eca9a5dd04b5cdf8a0dd32d8be8844dc33aba2bd + RNQuickAction: 6d404a869dc872cde841ad3147416a670d13fa93 RNRate: 29be49c24b314c4e8ec09d848c3965f61cb0be47 RNSecureKeyStore: f1ad870e53806453039f650720d2845c678d89c8 - RNSentry: 2803ba8c8129dcf26b79e9b4d8c80168be6e4390 + RNSentry: 9b1d983b2d5d1c215ba6490348fd2a4cc23a8a9d RNShare: 8b171d4b43c1d886917fdd303bf7a4b87167b05c RNSVG: 0eb087cfb5d7937be93c45b163b26352a647e681 RNVectorIcons: 0bb4def82230be1333ddaeee9fcba45f0b288ed4 - RNWatch: a14e378448e187cc12f307f61d41fe8a65400e86 - Sentry: bba998b0fb157fdd6596aa73290a9d67ae47be79 + RNWatch: a36ea17fac675b98b1d8cd41604af68cf1fa9a03 + Sentry: 14bdd673870e8cf64932b149fad5bbbf39a9b390 swift_qrcodejs: 4d024fc98b0778b804ec6a5c810880fd092aec9d TcpSockets: 8d839b9b14f6f344d98e4642ded13ab3112b462d ToolTipMenu: bdcaa0e888bcf44778a67fe34639b094352e904e diff --git a/package-lock.json b/package-lock.json index 1fc24ea3..7ac832e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1211,9 +1211,9 @@ "integrity": "sha512-EJGsbrHubK1mGxPjWB74AaHAd5m9I+Gg2RRPZzMK6org7QOU9WOBnIMFqoeVto3hKOaEPlk8NV74H6G34/2pZQ==" }, "@react-native-community/blur": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@react-native-community/blur/-/blur-3.3.1.tgz", - "integrity": "sha512-UfH2ut/l4GpZHeq/TGx3BrmyXSCSBBwBCVx1DhPodP3k959zJ2ajgXa3PiU/qjutftTUw6KH9Frsh2U0ax9dMQ==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@react-native-community/blur/-/blur-3.4.1.tgz", + "integrity": "sha512-XhbS230J7BGuoEamjPFZ5jUWDOW16y+vD0Soyq9Iv1qL8R47esGl54bnfUSMH10WhNXrQzvPxkMzap+ONHpE2w==", "requires": { "prop-types": "^15.5.10" } @@ -1330,9 +1330,9 @@ } }, "@react-native-community/slider": { - "version": "2.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@react-native-community/slider/-/slider-2.0.0-rc.1.tgz", - "integrity": "sha512-hdqQavGovI5M9NjCj4q4SPXyYOEHpBGXLIHBFETyL0S/B96hb09MXZAhOxsPYYs8KamYSKh2IYKwZ8yEiikNSg==" + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@react-native-community/slider/-/slider-2.0.8.tgz", + "integrity": "sha512-FZC3wjYzHQiD7jT7ALy3QNccyLj9zQBRiKGBFr/QvrWLkVg5orpIJ53aYFXm3eOkNvUV+wjhoI9uCkh3LCN2+A==" }, "@react-navigation/core": { "version": "3.4.2", @@ -1395,13 +1395,13 @@ "from": "git+https://github.com/BlueWallet/react-native-qrcode-local-image.git" }, "@sentry/browser": { - "version": "5.9.1", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-5.9.1.tgz", - "integrity": "sha512-7AOabwp9yAH9h6Xe6TfDwlLxHbUSWs+SPWHI7bPlht2yDSAqkXYGSzRr5X0XQJX9oBQdx2cEPMqHyJrbNaP/og==", + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-5.10.2.tgz", + "integrity": "sha512-r3eyBu2ln7odvWtXARCZPzpuGrKsD6U9F3gKTu4xdFkA0swSLUvS7AC2FUksj/1BE23y+eB/zzPT+RYJ58tidA==", "requires": { - "@sentry/core": "5.8.0", - "@sentry/types": "5.7.1", - "@sentry/utils": "5.8.0", + "@sentry/core": "5.10.2", + "@sentry/types": "5.10.0", + "@sentry/utils": "5.10.2", "tslib": "^1.9.3" } }, @@ -1419,71 +1419,71 @@ } }, "@sentry/core": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.8.0.tgz", - "integrity": "sha512-aAh2KLidIXJVGrxmHSVq2eVKbu7tZiYn5ylW6yzJXFetS5z4MA+JYaSBaG2inVYDEEqqMIkb17TyWxxziUDieg==", - "requires": { - "@sentry/hub": "5.8.0", - "@sentry/minimal": "5.8.0", - "@sentry/types": "5.7.1", - "@sentry/utils": "5.8.0", + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.10.2.tgz", + "integrity": "sha512-sKVeFH3v8K8xw2vM5MKMnnyAAwih+JSE3pbNL0CcCCA+/SwX+3jeAo2BhgXev2SAR/TjWW+wmeC9TdIW7KyYbg==", + "requires": { + "@sentry/hub": "5.10.2", + "@sentry/minimal": "5.10.2", + "@sentry/types": "5.10.0", + "@sentry/utils": "5.10.2", "tslib": "^1.9.3" } }, "@sentry/hub": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.8.0.tgz", - "integrity": "sha512-VdApn1ZCNwH1wwQwoO6pu53PM/qgHG+DQege0hbByluImpLBhAj9w50nXnF/8KzV4UoMIVbzCb6jXzMRmqqp9A==", + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.10.2.tgz", + "integrity": "sha512-hSlZIiu3hcR/I5yEhlpN9C0nip+U7hiRzRzUQaBiHO4YG4TC58NqnOPR89D/ekiuHIXzFpjW9OQmqtAMRoSUYA==", "requires": { - "@sentry/types": "5.7.1", - "@sentry/utils": "5.8.0", + "@sentry/types": "5.10.0", + "@sentry/utils": "5.10.2", "tslib": "^1.9.3" } }, "@sentry/integrations": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-5.8.0.tgz", - "integrity": "sha512-Obe3GqTtq63PAJ4opYEbeZ6Bm8uw+CND+7MywJLDguqnvIVRvxpcJIZ6wxcE/VjbU3OMkNmTMnM+ra8RB7Wj6w==", + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-5.10.2.tgz", + "integrity": "sha512-yvIJ6aXpzWlwD1WGTxvaeCm7JmNs4flWXKLlPWm2CEwgWBfjyaWjW7PqlA0jUOnLGCeYzgFD3AdUPlpgCsFXXA==", "requires": { - "@sentry/types": "5.7.1", - "@sentry/utils": "5.8.0", + "@sentry/types": "5.10.0", + "@sentry/utils": "5.10.2", "tslib": "^1.9.3" } }, "@sentry/minimal": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.8.0.tgz", - "integrity": "sha512-MIlFOgd+JvAUrBBmq7vr9ovRH1HvckhnwzHdoUPpKRBN+rQgTyZy1o6+kA2fASCbrRqFCP+Zk7EHMACKg8DpIw==", + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.10.2.tgz", + "integrity": "sha512-GalixiM9sckYfompH5HHTp9XT2BcjawBkcl1DMEKUBEi37+kUq0bivOBmnN1G/I4/wWOUdnAI/kagDWaWpbZPg==", "requires": { - "@sentry/hub": "5.8.0", - "@sentry/types": "5.7.1", + "@sentry/hub": "5.10.2", + "@sentry/types": "5.10.0", "tslib": "^1.9.3" } }, "@sentry/react-native": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@sentry/react-native/-/react-native-1.0.9.tgz", - "integrity": "sha512-fe1KEUJc+N4vq/k0ykqQ3el/CXxwTN7E8kBTYBCvxt9U449FdRodyFduiwz1UrkYhBsaQDC+5vQSZBZyT8EuOA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@sentry/react-native/-/react-native-1.2.1.tgz", + "integrity": "sha512-JE2B/pMvd7+3TFdzs03+DOdrALAHd8bAphJ8tk+nWjX7oQVJNgVn/IvnJfKxasHHBXQ2z+42Xy9n2Fqam/Gq0w==", "requires": { - "@sentry/browser": "^5.6.3", - "@sentry/core": "^5.6.2", - "@sentry/integrations": "^5.6.1", - "@sentry/types": "^5.6.1", - "@sentry/utils": "^5.6.1", - "@sentry/wizard": "^1.0.0" + "@sentry/browser": "^5.10.0", + "@sentry/core": "^5.10.0", + "@sentry/integrations": "^5.10.0", + "@sentry/types": "^5.10.0", + "@sentry/utils": "^5.10.0", + "@sentry/wizard": "^1.0.2" } }, "@sentry/types": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.7.1.tgz", - "integrity": "sha512-tbUnTYlSliXvnou5D4C8Zr+7/wJrHLbpYX1YkLXuIJRU0NSi81bHMroAuHWILcQKWhVjaV/HZzr7Y/hhWtbXVQ==" + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.10.0.tgz", + "integrity": "sha512-TW20GzkCWsP6uAxR2JIpIkiitCKyIOfkyDsKBeLqYj4SaZjfvBPnzgNCcYR0L0UsP1/Es6oHooZfIGSkp6GGxQ==" }, "@sentry/utils": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.8.0.tgz", - "integrity": "sha512-KDxUvBSYi0/dHMdunbxAxD3389pcQioLtcO6CI6zt/nJXeVFolix66cRraeQvqupdLhvOk/el649W4fCPayTHw==", + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.10.2.tgz", + "integrity": "sha512-UcbbaFpYrGSV448lQ16Cr+W/MPuKUflQQUdrMCt5vgaf5+M7kpozlcji4GGGZUCXIA7oRP93ABoXj55s1OM9zw==", "requires": { - "@sentry/types": "5.7.1", + "@sentry/types": "5.10.0", "tslib": "^1.9.3" } }, @@ -5354,7 +5354,8 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -5372,11 +5373,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5389,15 +5392,18 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -5500,7 +5506,8 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -5510,6 +5517,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5522,17 +5530,20 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true + "bundled": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -5549,6 +5560,7 @@ "mkdirp": { "version": "0.5.1", "bundled": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -5621,7 +5633,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -5631,6 +5644,7 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { "wrappy": "1" } @@ -5706,7 +5720,8 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true + "bundled": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -5736,6 +5751,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5753,6 +5769,7 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5791,11 +5808,13 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "optional": true }, "yallist": { "version": "3.0.3", - "bundled": true + "bundled": true, + "optional": true } } }, @@ -5902,6 +5921,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", "dev": true, + "optional": true, "requires": { "is-glob": "^2.0.0" } @@ -6418,7 +6438,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", - "dev": true + "dev": true, + "optional": true }, "is-finite": { "version": "1.0.2", @@ -6450,6 +6471,7 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", "dev": true, + "optional": true, "requires": { "is-extglob": "^1.0.0" } @@ -11046,9 +11068,9 @@ } }, "react-native-quick-actions": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/react-native-quick-actions/-/react-native-quick-actions-0.3.12.tgz", - "integrity": "sha512-jQkzbA6L1/+FIqvHnPenHUe2IrBihh48KOMTOa0QbOaxWzsejSB8kkWpSQOjYxjGu7k+3DVgosQ8wGJTB/l4JA==" + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/react-native-quick-actions/-/react-native-quick-actions-0.3.13.tgz", + "integrity": "sha512-Vz13a0+NV0mzCh/29tNt0qDzWPh8i2srTQW8eCSzGFDArnVm1COTOhTD0FY0hWHlxRY0ahvX+BlezTDvsyAuMA==" }, "react-native-randombytes": { "version": "3.5.3", @@ -11254,9 +11276,9 @@ } }, "react-native-watch-connectivity": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/react-native-watch-connectivity/-/react-native-watch-connectivity-0.4.1.tgz", - "integrity": "sha512-fMx3hRXinxAoC64YUL+UkXuwlgYeYp3ECKMucxFgRtouzfCC3RGD+wewY7vxgOrcjkKXusIbCdbU4gS2Lojhqw==" + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/react-native-watch-connectivity/-/react-native-watch-connectivity-0.4.2.tgz", + "integrity": "sha512-es/c1yPRc5aQXNjKuNr0nCgYvuD126bGDRAhq5OaKpnccWHGQorctqxKYRKyNjloOM/NGc97C7DDNnfF1cCfJw==" }, "react-native-webview": { "version": "6.9.0", @@ -11468,7 +11490,8 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "dev": true, + "optional": true }, "string_decoder": { "version": "1.1.1", diff --git a/package.json b/package.json index fc9678ab..c04ba613 100644 --- a/package.json +++ b/package.json @@ -53,10 +53,10 @@ "dependencies": { "@babel/preset-env": "7.5.0", "@react-native-community/async-storage": "1.6.2", - "@react-native-community/blur": "3.3.1", - "@react-native-community/slider": "2.0.0-rc.1", + "@react-native-community/blur": "3.4.1", + "@react-native-community/slider": "2.0.8", "@remobile/react-native-qrcode-local-image": "git+https://github.com/BlueWallet/react-native-qrcode-local-image.git", - "@sentry/react-native": "1.0.9", + "@sentry/react-native": "1.2.1", "amplitude-js": "5.6.0", "bech32": "1.1.3", "bignumber.js": "9.0.0", @@ -108,7 +108,7 @@ "react-native-privacy-snapshot": "git+https://github.com/BlueWallet/react-native-privacy-snapshot.git", "react-native-prompt-android": "git+https://github.com/marcosrdz/react-native-prompt-android.git", "react-native-qrcode-svg": "5.1.2", - "react-native-quick-actions": "0.3.12", + "react-native-quick-actions": "0.3.13", "react-native-randombytes": "3.5.3", "react-native-rate": "1.1.7", "react-native-secure-key-store": "git+https://github.com/marcosrdz/react-native-secure-key-store.git#38332f629f577cdd57c69fc8cc971b3cbad193c9", @@ -119,7 +119,7 @@ "react-native-tcp": "git+https://github.com/aprock/react-native-tcp.git", "react-native-tooltip": "git+https://github.com/marcosrdz/react-native-tooltip.git", "react-native-vector-icons": "6.6.0", - "react-native-watch-connectivity": "0.4.1", + "react-native-watch-connectivity": "0.4.2", "react-native-webview": "6.9.0", "react-navigation": "3.11.0", "react-navigation-hooks": "1.1.0", From 8889d5d347ac3e0c3a3284d7df187ec3792db33e Mon Sep 17 00:00:00 2001 From: Nicholas Chen Date: Wed, 1 Jan 2020 19:22:26 -0500 Subject: [PATCH 19/26] updating instructions for console compilation refer here for more information: https://github.com/BlueWallet/BlueWallet/issues/802 --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 57713590..6c550245 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,22 @@ cd BlueWallet npm install ``` +Please make sure that your console is running the most stable versions of npm and node (even-numbered versions). + +``` +npm --version && node --version +``` + +If not, please change your node version in the shell, assuming you have installed node and npm via the Homebrew Package Manager. + +``` +brew install node@12 +brew unlink node +brew link node@12 +``` + +For more information and background information, please refer to this link: https://apple.stackexchange.com/questions/171530/how-do-i-downgrade-node-or-install-a-specific-previous-version-using-homebrew + * To run on Android: You will now need to either connect an Android device to your computer or run an emulated Android device using AVD Manager which comes shipped with Android Studio. To run an emulator using AVD Manager: From e900e6e50e8b310c067f130877028eec5d8ac655 Mon Sep 17 00:00:00 2001 From: Nicholas Chen Date: Fri, 3 Jan 2020 18:59:47 -0500 Subject: [PATCH 20/26] Updated to make docs to make it more precise --- README.md | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 6c550245..6e74e0d4 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,15 @@ Community: [telegram group](https://t.me/bluewallet) +## BUILD & RUN IT +Please refer to the engines field in package.json file for the minimum required versions of Node and npm. It is preferred that you use an even-numbered version of Node as these are LTS versions. +To view the version of Node and npm in your environment, run the following in your console: -## BUILD & RUN IT +``` +node --version && npm --version +``` * In your console: @@ -40,20 +45,6 @@ npm install Please make sure that your console is running the most stable versions of npm and node (even-numbered versions). -``` -npm --version && node --version -``` - -If not, please change your node version in the shell, assuming you have installed node and npm via the Homebrew Package Manager. - -``` -brew install node@12 -brew unlink node -brew link node@12 -``` - -For more information and background information, please refer to this link: https://apple.stackexchange.com/questions/171530/how-do-i-downgrade-node-or-install-a-specific-previous-version-using-homebrew - * To run on Android: You will now need to either connect an Android device to your computer or run an emulated Android device using AVD Manager which comes shipped with Android Studio. To run an emulator using AVD Manager: From 2ec19bdec42864a24048b4e046cfc8c07706292b Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Date: Wed, 1 Jan 2020 23:32:35 -0600 Subject: [PATCH 21/26] REF: SelectWallet uses Hooks --- screen/wallets/selectWallet.js | 116 +++++++++++++-------------------- 1 file changed, 45 insertions(+), 71 deletions(-) diff --git a/screen/wallets/selectWallet.js b/screen/wallets/selectWallet.js index e409c783..3141e324 100644 --- a/screen/wallets/selectWallet.js +++ b/screen/wallets/selectWallet.js @@ -1,51 +1,35 @@ -import React, { Component } from 'react'; +/* eslint-disable react/prop-types */ +import React, { useEffect, useState } from 'react'; import { View, ActivityIndicator, Image, Text, TouchableOpacity, FlatList } from 'react-native'; import { SafeBlueArea, BlueNavigationStyle, BlueText, BlueSpacing20, BluePrivateBalance } from '../../BlueComponents'; import LinearGradient from 'react-native-linear-gradient'; -import PropTypes from 'prop-types'; import { LightningCustodianWallet } from '../../class/lightning-custodian-wallet'; import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; import WalletGradient from '../../class/walletGradient'; +import { useNavigationParam } from 'react-navigation-hooks'; +import { Chain } from '../../models/bitcoinUnits'; /** @type {AppStorage} */ -let BlueApp = require('../../BlueApp'); -let loc = require('../../loc'); +const BlueApp = require('../../BlueApp'); +const loc = require('../../loc'); -export default class SelectWallet extends Component { - static navigationOptions = ({ navigation }) => ({ - ...BlueNavigationStyle(navigation, true, navigation.getParam('dismissAcion')), - title: loc.wallets.select_wallet, - }); - - constructor(props) { - super(props); - props.navigation.setParams({ dismissAcion: this.dismissComponent }); - this.state = { - isLoading: true, - data: [], - }; - this.chainType = props.navigation.getParam('chainType'); - } - - dismissComponent = () => { - this.props.navigation.goBack(null); - }; +const SelectWallet = () => { + const chainType = useNavigationParam('chainType') || Chain.ONCHAIN; + const onWalletSelect = useNavigationParam('onWalletSelect'); + const [isLoading, setIsLoading] = useState(true); + const data = chainType + ? BlueApp.getWallets().filter(item => item.chain === chainType && item.allowSend()) + : BlueApp.getWallets().filter(item => item.allowSend()) || []; - componentDidMount() { - const wallets = this.chainType - ? BlueApp.getWallets().filter(item => item.chain === this.chainType && item.allowSend()) - : BlueApp.getWallets().filter(item => item.allowSend()); - this.setState({ - data: wallets, - isLoading: false, - }); - } + useEffect(() => { + setIsLoading(false); + }); - _renderItem = ({ item }) => { + const renderItem = ({ item }) => { return ( { ReactNativeHapticFeedback.trigger('selection', { ignoreAndroidSystemSettings: false }); - this.props.navigation.getParam('onWalletSelect')(item); + onWalletSelect(item); }} > - + if (isLoading) { + return ( + + + + ); + } else if (data.length <= 0) { + return ( + + + There are currently no Bitcoin wallets available. + + + A Bitcoin wallet is required to refill Lightning wallets. Please, create or import one. + - ); - } else if (this.state.data.length <= 0) { - return ( - - - There are currently no Bitcoin wallets available. - - - A Bitcoin wallet is required to refill Lightning wallets. Please, create or import one. - - - - ); - } - + + ); + } else { return ( - - `${index}`} - /> + + `${index}`} /> ); } -} - -SelectWallet.propTypes = { - navigation: PropTypes.shape({ - navigate: PropTypes.func, - goBack: PropTypes.func, - setParams: PropTypes.func, - getParam: PropTypes.func, - }), }; + +SelectWallet.navigationOptions = ({ navigation }) => ({ + ...BlueNavigationStyle(navigation, true, () => navigation.goBack(null)), + title: loc.wallets.select_wallet, +}); + +export default SelectWallet; From 78e82a8c5d08acedbe618e9539eebbe096993ad7 Mon Sep 17 00:00:00 2001 From: Overtorment Date: Sat, 4 Jan 2020 20:31:58 +0000 Subject: [PATCH 22/26] Update deepLinkSchemaMatch.test.js --- tests/integration/deepLinkSchemaMatch.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/deepLinkSchemaMatch.test.js b/tests/integration/deepLinkSchemaMatch.test.js index f852b56c..0c2d6194 100644 --- a/tests/integration/deepLinkSchemaMatch.test.js +++ b/tests/integration/deepLinkSchemaMatch.test.js @@ -1,4 +1,4 @@ -/* global describe, it, expect */ +/* global describe, it */ import DeeplinkSchemaMatch from '../../class/deeplinkSchemaMatch'; const assert = require('assert'); From 4078ed30c9ce719720477737b923f24384977a57 Mon Sep 17 00:00:00 2001 From: Overtorment Date: Sat, 4 Jan 2020 22:38:45 +0000 Subject: [PATCH 23/26] REF: electrum --- BlueElectrum.js | 4 +++- tests/integration/Electrum.test.js | 7 +++++++ tests/integration/HDWallet.test.js | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/BlueElectrum.js b/BlueElectrum.js index ada501a6..b7cab17b 100644 --- a/BlueElectrum.js +++ b/BlueElectrum.js @@ -295,7 +295,9 @@ module.exports.multiGetHistoryByAddress = async function(addresses, batchsize) { }; module.exports.multiGetTransactionByTxid = async function(txids, batchsize, verbose) { - batchsize = batchsize || 100; + batchsize = batchsize || 81; + // this value is fine-tuned so althrough wallets in test suite will occasionally + // throw 'response too large (over 1,000,000 bytes', test suite will pass verbose = verbose !== false; if (!mainClient) throw new Error('Electrum client is not connected'); let ret = {}; diff --git a/tests/integration/Electrum.test.js b/tests/integration/Electrum.test.js index bb6530b1..57496913 100644 --- a/tests/integration/Electrum.test.js +++ b/tests/integration/Electrum.test.js @@ -193,6 +193,13 @@ describe('Electrum', () => { assert.ok(Object.keys(txdatas).length === 4); }); + it('multiGetTransactionByTxid() can work with big batches', async () => { + // eslint-disable-next-line + let vinTxids = ["fb083ca5fb98451314836bbf31d08bf658deddcc8947d76eefe44b5aa49f3a48","5f0f4ac816af5717f6e049454ef698c5d46b2303b1622a17b42eed0d914ea412","8f58af45a9375ed2516237e6910186f4c9c5655a873396e71721057bdd68b3f2","e3cca3df55a880adbe639955c68772d2c491b36e28368380241ac096d9967095","5aef653da43618151ea24ed13b26e67415a397212a60dbb3609a67ba1c01373b","b67c9ce40020584b82085f4093d066c2759542d9aa612bd1cc2e561548e2f9bb","28f29df4660afa58dd6e0f9c31d3e3c204b575029c4b429dbab7da834748f097","147aa266b25fb24bfd7f2e66a1d8eeaa5086c35922c59a36a7a7c4e6a7805d5d","40f00b01d2173c46fc1b4f462132f183beb368bd43df7b88869a8bb7a8bb3b5f","b67c9ce40020584b82085f4093d066c2759542d9aa612bd1cc2e561548e2f9bb","147aa266b25fb24bfd7f2e66a1d8eeaa5086c35922c59a36a7a7c4e6a7805d5d","ba4a6c71c3b3d738609526693bdf87ed5b441f3546d70a2a83128cf6645c10dd","b1b7410fb84def2a3fa0ba54e587e3ce373c4f6e1972b5a7044c804e53101131","a4a0790f7d00ad7da97e349062cff61da12bc2b1ee06223a41d4960621ad6781","b1b7410fb84def2a3fa0ba54e587e3ce373c4f6e1972b5a7044c804e53101131","15e499177b871ac205bf509e1abd81c29a72475661957ea82db69f4afde6cc3f","6702bf9dc679c9086d0cb3f43c10d48bb200e1b29d73b4b4a399bd3eae31f4d0","51c07c5c882b49931d18adf2f62afed25db53ef4264b1f3144152b6c4dfeda1c","38eba464479c79a230e0256b7e9cc9b9cc8539b9a7297bc9961046df29d63b4a","5d29eef47330797e9b37cf0d57e48a871a6e249a0102e4744ec9d30613b054b8","3af7c1fa064a2c74e0d34972895daba4b78f32eae596774bb4bb35d44cf069c1","cc5485827920ea5de1375c9806a1b6f7db1f87f2e4074481778d1e44183b3cf6","38eba464479c79a230e0256b7e9cc9b9cc8539b9a7297bc9961046df29d63b4a","217b99f47c1a05076b68e98cf65b08df8aa485c28c74e43a0ab5a4aa98a89fa0","fb2b8dcb2997c331bd8d9f4d611293874bde116184ef38dfd5a0d5745f9d475d","2ef35d16748d56d4ec19fb458bc0147e85dd1d78e25f71f0ddd71ba22f0f49ec","05b4009d2ec7c6472a10d652efdbd7e4fa72a2e2448287e386c4d68864ba75af","cd801bc012c361fd6be0bc5b440c71d656d2fa14af90af1f469fc91ca3be0967","24807d3563342275bee7cc4b98dc2fadb9e39d074534e79c7921b46596988344","2b29aaa43d344393769f38e1d3dc0ba1d89b2f9868311ff9ec9eb44183d21250","b07747a598d4f1f0beae08332568292104f4360929057396c5637c1900e4f3ff","f13a237529bfd21edad34678f1da8ebd4606422a85b5f92ee270f3808c286e44","20264987b20aa1d6975cf9bad62855c3f8c1492e38ea7a73963eb355bd18b77f","65e0a29033eb3cea6b4c9eaa7448754e468585cf1877e0b228281138421f30cb","133874b546cdc715439d1d1dba865419a9640ee740f8a72191d1221ac29d066b","b056e86b160b08d3e944f1d61368d3523e9c0820ebc8f755b98ac38eb3e307d1","4bb01e9b0388441442a04bb36fe932c3e3da525c4c09413c90f0a6a74c57d280","2fc80afde86a45f5f113067f44433d44da28d6e17dd8dbe19f511bc371863a1c","e7650f7fa769c2b1aee41477278633a342017c81009feff8b6566b3f3664b6dc","01315e63a7b852f1dfd83bc7a405351492c076d88f2ae4adc3c32d3431fd5a35","7cb1e59903fb72e4f618ba7ab078765e9117d8aca2421268fe24b41986df37ba","7fd27c38816b461eba58b547a524747b28eec2b58e2c8fd29fa9858de96265f5","38b95ee03adf62feb5188c1a8c5ac6334e95e0e21bba6b30e667b54f6ad7b728","8927bc71680f4a9f25af6f2c0c4e1956e0e703124ac725d842c64bba5e92a6e5","48d0b1d45f8e228d41f01f71e56618d0f2ce8263bbd9b59352760640eb2df66c","8f41920019edb0924b861daa6feaf2922d0ce0a9c45b777ba88f6e026cb9a6ea","7a4dad8678a23b608ce8acacd9a2f4c24929bc68037fb2991a43cc022e977b60","684daad794ebc87661f8250eb42621db03f6b3f31185e0f760ee3db73286e6c9","69080677f9b643e100e62a2edf12e2dbd529b047d584ff922e5458381761a05d","ccc4c5111cc02d243b67fc20fd93026d88add65c6fd2055602152227903c703c","242fc7936d4480e5fba4b9515aca0b97de121a9effcc1be26d4e455c7adef9f1","833db41cd8b9c20470bc3b5e70249d243aacfcf6b69593ac460a0029467689b9","5fd098cace35e4c0d08f10059d520bae1b2b074ac1059d2980a1be82a8f77f31","e445ff4d77e50cd5d04d2dd3c482677c5531c2198494768dacb624d4f7f7bfc5","c4e521e8f00cc3979bdd0e0c99970374fa1f92ff742a3e9fdf52c1e4bbcfd78d","a1d6296a9801ee538e0d27feb10f8be41b00b0fccc6c83fcb49e98f23c42dcd3","aa368285e0e05d9bc3f957b89b95260349684cc872b167f36a61982bf63c2201","f4fa04c284d1f3eb907f9e7f8adf6e1e832a21ed763d6d23857e45926f9ce10a","fb9e869cf8465c71c8f4a1756a6dbd8dcd1eca4246d8645b037ad0cc65e800f0","e059ad921e8a07dacd01727774bd17821768e61d3f7062f77131f517a9197e50","8736be2b2a646ce5a471e16baf9d11aadd818a71dfe790395b0b2809e115c1e6","4fe6b558e435fdad3843b2db5d14915501c147148dbeb4985e3e596feda17d99","e8318e5e612c6d48b4711e5414abc396ab3d7633a2529eaabe3a82d7c851ccb4","804b049e776174f3eded63d18185d769841f7abc5b0751109a5942862c939fdf","3d263a914bb8e82f1566490d2e8d20ab66542f223089cc4cce9b034930e775f1","1daf03c9dabfd10483a637b59f18d44b8e5fc8a4b2d44e72ffa610824f71879b","3bd025508217aeba5c4a616ea6c30969f252f58231087ce11714db7110b2005c","1a66088cb7d460aeb56c00dd069156adc5cef549648e5481ee5c5e589879b731","4ff1cc8b15a09062bed7a53124c3af351ceda82d439aa3606fe3ca277a20bc2a","ca2095f1eced85cb8e07f785b4e1b8c3f7d9e8ee60f87458c40bdfe731d09c30","d7f31a681b35a7012ced38f106f3b478e9a31f8d58fbf244c721ca1ba89cb394","4bd47e0c626359950fc09888e1a505dfe7286800d0ec0a21b49c66d8de32c901","1a66088cb7d460aeb56c00dd069156adc5cef549648e5481ee5c5e589879b731","3bd025508217aeba5c4a616ea6c30969f252f58231087ce11714db7110b2005c","19ebfbba4c51e79099c031e91a36621312a50c46d0f87d83183a71a0834afe3f","e8ebe53a94ee73fa803f0f144c8ac31d5075de45e2f798b383a6d88164d6113a","944569d8a887d238cf2d7c104ea3fe558bdea38e961b73a0378bfd7969e13157","a08897fe730d7de1f2d5376e1d6e9e500c6e20dce4bd9225c5694667464bf8eb","472a92ca21fe52d676dca31266943a87745bec8b04450b83e5ea29559253e902","3c86ca512b36c13218249dede37b9ed2813b368115926931c08df7dfb049822b","b49c0fce3665545d887872e927e76d7df0927e4baec27c7106181651611424ab","8881027cfa2be033b7a9724b5547b710f5ba8aef1becb3ed53d34d614a54e3bc","bb5ee6ed47f61fc2cc33c29540f1e778a9c9583083e2441c9b23439c5e46820f","cbd9c2e1782d72fae42254f615caba2cc28de5f222ba142ff2020c5499990cc4","52b39e96c6672ecc70554cea0a10c57ad4636526a8f51477d2108bdda78d30e8","c2c4cb3164f6576cf559ab0e7180ad3d1f5ba27156727085ddf3ec36b4fccf33","af940582b1416ab699abc542994f2f8da43c0af0c5518e57c2087eb33dc0503d","d7eace81b9bb79318113d2f30f9d18d90d78f0dffaee7f3bacbcc343ee0101cd","aaf156826bb506f93fc4aaee3bed00172819f02a06ecd5a446afce5321f93ba3","ca2e3b7dde57407212bc4017621f6433908dc2f8b725d365fc7109c7b8067097","d6fdbf77be6e2979543798f41b6cf36fca557c17d0a9d367cc52701770eb2d46","1af6c454434e19d802f83f600982e083de6b4f925c90938783c55236174640b2","66a5abcb3210724cc93b9e4eb537ef9f28e987f2915f00620eb887a419521125","91df6caa3a703fbaebaf35a3ed61d7b80eae59e0daf3f4443c41e2ed69d8cadf","cf62992c60c36cd327652e93bfb0fb7fa14d1f80195616907128ca7fd2b8b24c","1cf9269f1c03276f21bd0f939ee83f9319df7516f9efb06cdbc8ad4f2e985d15","39e051909a54e187a21500c3ab48966058414320a881dbb90517ef770e18b553","696d6bab49f36a42c10486bb4db41f6c7ab89e1f615c221a3c8115b9813246d6","ce01648dfa0c14f86836404e745a9d071c992675d8899b837233a1ccf6e8d9d1","6e60a751d87946c28cbbd6d19115a036edc083b61d981efe513327bbe4e035cf","c73a7a623ffe18351b51942f10371950a5afc5b967ad8e831fc1b4339078b004","2ababd951785b88b19c609338df9bf5f3b25f33d85196410533dc02f6e3d2e60","4edfa2ade6895b198d53dfe5070148c920cc55e810ab4b7c109192d045289669","be8e83fa02962e432fea91f9d49fade6a781c8e85d277ac4f90ab73d7ec1c75e","297256d3db749c28489984d33e3a44118068c71dee801f6c8f7e61dec30e7103","7df23870105909c9c390e97527ea9a37f5fc4dab7a2d11b5bce48ac97e81fcb5","5fcef9d6d418f7a47bbbff082d97b4cf789403df2a0eb75f1ae18b873362f8fb","b3ca2274cc0a5bd844df396ee075344a210fa95b50738d6f8938a76b865fe874","fd2606fc511fa58038c9e9511f3db2d0163ee4615b1c58af53ad1c419b85a7b7","75386c3230b6eab0dd67207ade52679d6fb3c8f9003bca870e0bde8980971146","35c7d91dfcecac4ea844a26f4650e0c7455722359d2299f265ab609da6a7e885","7d67395f69c72c319b991b8c0a8c550cdc7124aa73b86edb5dc78125c8cb2b06","45c7b3879a07324548d034d5698166f8e0fcfcfdf596c18640fe3f7636913bdb","a069791dbb92acabe3ade5dcc6f88f4310abcd6e77927764c71db4c850190542","60c8f1368add0de87f0849d0e93a92d707fecd6f476a850f242dd2c283e05fcc","8d32423d43a5a22332c312975ed03e1e8131dc35c846a16a7cfc3abc255b8eb5","9717ce2d17c080a6bbc635ec15295d0b6567442f8f6e247e56d61a35fed8f574","618d565513599b0cfcee5ab01f6ef86aabaf11615eafd56f66eaba29df6073ca","e53cde1fccab2a904f542a9cad704843a71964be0223113d8c229a706b1d3120","ced6089c414f9deceb70228e42ad0281ec8b13ef4f3a8b3952b6417f9f69346e","b1cb1f5a8b04e3cd5d4e736ed3c703dac8db6376bfa74f4ca5ed0264ffcb6992","326b5964ab27e31c3952520bae0e06c66daf576e3a70faca2a891228831cc3ee","40a0b058213a2e3540c9fc3a5dcbd1eb5bd876c9748f81bb12dc89a944e45a29","12bc66fa8c86611d2785bec80faad4b880cd48bc4ff30f337022d9e8675e30c4","3a7ef749c28ab552daa4fb7e54c3980e86aefa92506caded4feedb4bb70a03c9","4ed789a4d9adc6777c585653267b1d22292d992adfb5224faa1115a1c2b3734c","dc0de14bc6290725ef57d2a54a4044dbb83b425af4a94d609ed7ef516d4d03b1","bc83071647ec91f147f83635bcf99faf8f3431b0af41094ad9a574f117618adc","adce7a0971cd0a515e31e761f4f76113347548d792e0d14cedd683c9221a006a","8fbc8775ab4a90aabc860679fa9ec02be810d1076b607f0f0573baa37ca759fe","ad24ecdfbacb2e8f49425aec400f028e3cc26fb2f0bb7e1dda3ca9ccd98b2c65","01864c0c6901c8e0214987703d75b6f7248bde19964d1c3460dcb8b447296c52","91bcfb9559bb4ea9890c403cc8d0420b4312b4f1f233838636b1f1cf0d03cca1","74ea7961e345714f014bea53111b23952f5309f318af4c373388308971c481ab","1a0df705f0bdf61d9474d949395fbed08f9ffa73668b1201ff6eb4be6ecc94a8","d65d0447197ac25105c8b6fed1675be1940e595eed94aa552140d0027c3a14ae","52d1fcdb4899521daf0b36fd46e0daf8a0e3d1fcd80246f8ac6bd9b92e1bf814","fc26e159b9ba609a0c3abb098f800ddfdc124f0150f6bcd6ef28a73edcfc46b1","fa04269ada73e905ae9b70e0b348c04e980ce0c9a5f92813642691c90fff9b4c","6f67910e966aed715c97a3f34123895e5f12cc3eac821b35519f14410ed84217","2e3f3493cacf15681f4587b2ddccf16bf24c93025cb62b5c9c7c725303c18e42","ff5aaeb07738cecc1ec9c5959254005fa20ca2fd331dd2f15dc80ad6dd8810d2","de1e6715cd6c6ab48a4fed996c168d40cbaa8a47356deb9729ae3df1430e6ccd","b005828433393021f9db66d6e8ad6928f0a9d5c3dc2cf4dbfa0e264b6d4f9e3f","8e67b23999d09515a11fcb142bd1c843580eba1e07df1a89e97bfc61db430739","9a69f53c94388a60f1b16f7d2861bd554fcd7f6aceef6b1c1e3f48ca9030fbff","65ead8101d45973f10664e840193ae01ff8179a81e0a143e6a4cb90a4f0a78f8","ffa35cc1ab3169651692b80f3c91a16ff5136d7c15fba57f43101fc31b9c45f7","c3f038db69e3d5f661f109b711ad04e7f596b68b2d555a081ea422a18109016b","2a07e0970916588dc3d1cf8274c0e54672232a55cc01437deea383fbc9fd8903","6ef42127f51e88ba7505c920e77897d5b983bc86955439492094f1966bb17dd0","d22a5dfe87c858a69e1470dcc8d2fb14dc7b1c30002ca6c2ab85ee8d11f26c01","088131dc57db44873dca6ecc79ac8fbef7d37dccb1618ce9373526512956dde6","4ef88767cc4a4234fd487165ca4d733eac96f5d0f69d0da3668509ad8322a6e7","0b9171dd1de9b00df7c5ca401acf9d715cc5a1c4a77082996beb166e661197a8","dab64e620f07903bd218338db299f48cd21a578e18b54d21d75d9f887d4cdee5","0f357a93a11c2921ac7d078ba2fc01ed6142141ad27170a70d6ca1e7bcd216da","935d138f548875f2700aad732286be9f9b7a27950e49b8a9d9a26d4a04984849","97049b7cc03e75ca4d88e5fc499d028e0efd36426f983e7cc62e79ac25bacd74","29ce849f696e358d3d191165dd0b5a0fe877551d8076a13545bb42e4c1f2628a","d612e39e947aaa4ead5ba2b9f117f9f0f66eaa7b6f8e9ce3fad27840d048c4bb","2f9e1b6669b51121d81ee2f8e7ff070d55501e478b27d9bcd5b517037dbb907a","09fb6c5cf76713fc1aa2cba8cff23a15706ebc69a0ace264ab97b1d27f87b25b","dfe8efc67c47bc2a09806ef1a7b795223b7d200fce0a41173382c8d5d15f4e1d","25c9af80002c3cafcf9a71a115d5cf1fb52ee5e2bbb92e7fe5af2ac498bdade1","dfe8efc67c47bc2a09806ef1a7b795223b7d200fce0a41173382c8d5d15f4e1d","d3f431413d390d24af4f8bfa5f72768f4e1dfd2d84ae1235a27961405c6b8660","d3f431413d390d24af4f8bfa5f72768f4e1dfd2d84ae1235a27961405c6b8660","4bbbbc9591fe8270da47b29df25e14322e6560ac547382ae2a42ee35ecaa44bf","d56c5203a816443e1823372533b711434c45e25a09e5cedad076c9d8038065e9","44c9d492e1374aafb3f6d71f8f408eb74394550ad8e4665c0169bb7dd930ffea","11610aae481891ec2a0fc552d7f3c569d6ebcd8150c322c1d58b94f3a0f1c3df","3dac2a5afb3d345b6276c42ed4a69aee7b9600e0562ad6d3a9828fcbdba78267","67c759d4395f8b91e82a43a13be121e6924def3f3ce88c6a62bd45ec04e2a0a8","d6a7a8d9fe420579dd5351ac769cbd5c1d2454ef8f1b142a71cef10ecc70ec56","7f34eecdda77971fc96007d90eebd2f7addc85c214e0442f40990a28b840a032","153b939a629f12f0802d21bc0b335bb49daef00c6b9c4535c6144883a02a05a3","2f61964ca74a3b0f3c845426395a2d4cefa51ff5b0716bf1d9933274c40cefe9","83cb85c0863bc46fbac3879b4c67a60629008c02eaa88ddd1d51cf8d6013eadc","1e7c81072f87b7d817c8583a78a4d4056178b21f9ecf3d237b19bcc8cee1d391","736af4be184ea560f3010dd6bd796846e773e223f14ba13c1b9cec272788126a","0addef1e4c530c74fbaa56d65b00e9a0929bf64375fa6bf848d8afc8e1da3c3d","77bfab96a52e4fbf3b9afac9447ca6be2c64564f2c58fc560f7104611e646128","af32a73e481f2825f2dd83e5aa06fea0ee2d064a997a203af95d5e33ccf91ad5","37974afcd645a15237810a6785d78cfb51d791d6cd074b96770781248675a838","e0374bccc25125fb6b18555d7ec9761df443bd5801185359534ecdbae8c34a05","e26c3a19188fc8e9fee4962df6d30cf57d238463e79e92c7c65e77dcbd317d8f","17a4fafbf186721a4088d5f3d4373c74bd00af9a86e09bf5f056ba72c36748a4","41a0d9e099ee0c58affff9765cb0536174b98eda38bf403eaeb4d7f014180a48","b032980d1dfdf8f6692c1eac85f7d24134c075e6071e1cc1453a340ce04c94c2","a2fa19621c26c64773920e21c5ddd2758bdd992759a5124e1b4ae761d31d81b6","2a97222ee8383b85a17cdf9e868d1a04eedc5a7bd2b9813f1333fda730fd8149","7e278244c39cea3270f540d7add70595cb73204951030e9a6062ab9f3528a20a","f7be68d1757c477d837c754a5aebb96d44e7668d74204a6e471101acc2e3c331","0de6045abf24a55ffbe03db5517e1b619239e76d9308681899e6c115f10936bb","dfb894c450cef578aa27499be753b8945b759a6006f498e8a808da9362e60420","99727700bf127721388f7ebbe23188da7e87beaee64f775b0d6dff3e6ee9f499","09bbfdb57c117eb7b11b386d388923f8e6caf2a3c6b29fff81041c5ce427f6f3","3d3a1c0602c0e00c01efd86425ae6cdf9abc81fff15679bdcdaccea56cf992ac","4bf67246834d6fa98ec500cb519f39afd2159b4f932f7438c3f8afca62781215","8d2c3da3e0514f5f4dc88c4157bd320f89101c392be1a323b6eb42e94a47c415","fcf5d4d250a4967064b3c571f7e63decf50802e2ca6ac841cefe66aee6b6f1a9","fc44a49a0431b10c27c130da37ee6dcd1b0dc695d0b65ca6f57b89ca32030d69","ac58809688940109655913fea3cd3f4c37dc3192124d4b3bd6597cff4a9a6fc4","034243891e1c94fdf12a7008ed3db1e162c39a6b722e2226615e9d28ab233a38","acc6c6cd91d8cc9b37bcccd7385d98a95708b79ebd3a048b84499e33a988329c","90fb75c59748fd82c8052bb7ead9623d5a8cd1f8d0d75b743678b750a61d5647","071c3b31cca18e2c879dff4a8202cd2fdcb25045b64b6f1a035c5a712ba6f4e7","327e9f1a1b1d630314b5fe11e3cd5b2b2288e1044d3b98cc553523cdb765c5c8","6fe963af6a8576cc6190979a7fa46f71100d7468f01ae6fd3a1f60d293d95fec","d6adfc9ada50609dbfd6f598a9402a235f270aeb895cbd3433bfa1611301313a","0dd9f649e2f2afa45cd17181f55a01c76cb3f631770ea75952c4ad48fbd57d72","89622477e7d7cf0ff4ad038876b2231878a4f21976f6949af7b6876e0a0ccaf0","ddee132dd2a5b78962d0e6195a4990646e8d4567df5df058eb8280c2854b1b8d","d030c924c80a977415ba752a59eb8c359fc255608a72d137e5a3995e4f3dd09a","55e93a772bb8fd4851dfb79014d40f11d9dd714eff39acef6045776d6ee7abb7","79f0fe9adab2a9c45054ca0665649f702284d435fcecf6963f426494c4685177","9350f517362ea8d2e2a14ad23e77820b7f811a57b740ac145aa9607aebfbbcc1","f20bd6e5f63e3751b65cc0f69f800d1386b71f7bcf831df63a3e0a1980940f3a","e58d90563ede6de8f27a04736ae935f918c840b8c4d3ba2e9fd37197bd6dd776","a26368de811b9f9b4982bf58d3dcf926929b1768c9140cf52e6628f75699c92c","70b58e0eedf00c7424eee4693ed777f70cce8d9db6a3d41e7e7f911e1f3b7f94","eb5768d60f8b0323f07f7e89c0879617fedc960c37c85cf7c25c50e0165f0597","a90e6b3648ef7e2cd8951ee712a1b186c1092a11a149a1ddd96b97e01ce7d4f0","d2641c4410d3add40bf7a4dbdbc80c3f056c2a5b15fff09282eb06022c951ed7","8732005fd6e34080693fa81e4227187c05e70cefa43726fe459ae1b80a041437","124e5d8278708bd1a32a51c452508e15dfd06e7191c3994933cadb8f072fec4f","27f82563dc1dfcf0172267da16a4adae713cd2980e02577450530f36fd9aceba","e5dd6efb0064688f3857440b97e231045c4a1b3f708a00cd18ee721b4ac71db0","1aa08a948bc8b6bd3285973f97a14e4ad4a04b8c1cd696dbc4654f2689b3b4a4","20acd54cc349c30313c76e850d2d9a381540514d6330722fbb4d17c1d94fffcc","d934c22a6d997a8a1ecbc6c085587a52ec21a29d56803eb6e40d0c5f7bf79761","02ab03784d559a467cb4b720a8ce8f203f6968c2ecff93a79114fa13f7f8a3b6","01fc012c78c3f28beaa494e3b3cc45aaeea52923e24c1ea0bf5aaf6715fbddb8","e737cc7650ebe1801bbd87523a015431a9a2b44d090af8d84ff780e2fc0a7570","d8ee63e1d52dc44c682966ff8fc53b0d00646400a92df4adf02cfa0418c2823e","b6dfc2022e2a716dcf0e5c8057126c03fa75844e893bcd9461d2c24172ca0e0f","231cc8f40e3456165c8667715a40b9538bfb73d48668aca51db481b0b485bccd","f289532af57cb7e80c0f670205b64d27d2b9c6cdb6f78df475b585cbbc2d1a69","0a766fd0048fd37119a4bdad28dc2c952369a3e8fcc04892dd39adbd3700f818","d1cd8572518e8bbd33be0c841ced2e9bbe9d67471f27e4b9374c26b8e62473d9","5fcef9d6d418f7a47bbbff082d97b4cf789403df2a0eb75f1ae18b873362f8fb","42362a794b85db5592eaab0e21c9b6894a932653d59a97f3e081a7f9760ece61","70631829bbf1208cb277a988224efeedff692a6a44bb0d33b99a5750652cc2e3","269c8261cc14dd76365a9ed49f71332c0ce95198a51746c4343e6a8c99842bc7","7219f03e489efd3648c5182bc96a3f7bd9d39c1d656d25ae57314ab281eadb79","09fb6c5cf76713fc1aa2cba8cff23a15706ebc69a0ace264ab97b1d27f87b25b","2f9e1b6669b51121d81ee2f8e7ff070d55501e478b27d9bcd5b517037dbb907a","3879367de390469ac48196f22e461e58e03545f79e4ec5f6c4f935db5f26c7a8","4e65324ae5d788ba5e8982eb9af30ef9db63d454576aee979b183bdbf822bd10","18f46b4609876d1d108434c8207317f0f11a602aba8f899e3371f91887442bd9","23fd40eaa32f83a635fdba9ee1dbcb2308a3f6f651428c339dbe083bc10b1379","4785d461d01c6c9ad6fb294478df3ff76f677bb239b1434eea096f18e82508e1","f402bac0eae54e6d72b832962e2224eeddd330ae8df85bc1e02c4e47c831fd9a","d2da86368f860bd524a9bc9a6a3fc68fd822e18d921e20a68843e41946aaea20","07377d1ffb0e5f4582c311e11e5d5598ffd9d4b40d6b431a783d89d34e9597ff","a06e59909846a40b8ff6936ccb25279ae7152f0d1213cbcc52bc702601b1af1b","3632eb0ec85c5d95efeafca8a6237cb594d83f98779bdd763edf2e79b4e9e19a","387eb7e0dad26484d9f00d39e40658e4271d1c82b9f0c0365a211a12a536353a","090ce294c0ba760c0338591af254b03ed4310defe2fa6c4ff8096de17d3ee856","e0b84355712e3f0c4cc076a40e96c5991667ee17b746db73f52e67f5b1d59b1c","5444a9606d8aacf4b8f4779a00df1fb9f28b6d275f519011b6c511ff2419b745","f59ea6fd64694fdb9d7e94577a33a95725967b72bc01069a285e52caf78704ab"]; + let vintxdatas = await BlueElectrum.multiGetTransactionByTxid(vinTxids); + assert.ok(vintxdatas['8881027cfa2be033b7a9724b5547b710f5ba8aef1becb3ed53d34d614a54e3bc']); + }); + it.skip('multiGetTransactionByTxid() can work with huge tx', async () => { // electrum cant return verbose output because of "response too large (over 1,000,000 bytes" // for example: diff --git a/tests/integration/HDWallet.test.js b/tests/integration/HDWallet.test.js index cc230452..8c43060a 100644 --- a/tests/integration/HDWallet.test.js +++ b/tests/integration/HDWallet.test.js @@ -95,7 +95,7 @@ it('Segwit HD (BIP49) can fetch more data if pointers to last_used_addr are lagg hd.next_free_address_index = 50; await hd.fetchBalance(); await hd.fetchTransactions(); - assert.strictEqual(hd.getTransactions().length, 152); + assert.strictEqual(hd.getTransactions().length, 153); }); it('Segwit HD (BIP49) can generate addressess only via ypub', function() { From 7e8c21677784e44150bef311550fd1c9ff46bd1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Rodriguez=20V=C3=A9lez?= Date: Tue, 7 Jan 2020 17:56:33 -0500 Subject: [PATCH 24/26] ADD: CLP Fiat --- models/fiatUnit.js | 1 + 1 file changed, 1 insertion(+) diff --git a/models/fiatUnit.js b/models/fiatUnit.js index 6e432dcd..e34ec74b 100644 --- a/models/fiatUnit.js +++ b/models/fiatUnit.js @@ -4,6 +4,7 @@ export const FiatUnit = Object.freeze({ BRL: { endPointKey: 'BRL', symbol: 'R$', locale: 'pt-BR' }, CAD: { endPointKey: 'CAD', symbol: '$', locale: 'en-CA' }, CHF: { endPointKey: 'CHF', symbol: 'CHF', locale: 'de-CH' }, + CLP: { endPointKey: 'CLP', symbol: '$', locale: 'es-CL' }, CZK: { endPointKey: 'CZK', symbol: 'Kč', locale: 'cs-CZ' }, CNY: { endPointKey: 'CNY', symbol: '¥', locale: 'zh-CN' }, EUR: { endPointKey: 'EUR', symbol: '€', locale: 'en-EN' }, From f5409d74e455770d2a008ac241af13dea77390f2 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Date: Wed, 8 Jan 2020 04:29:51 -0500 Subject: [PATCH 25/26] FIX: Camera activation --- screen/wallets/list.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/screen/wallets/list.js b/screen/wallets/list.js index a52ff288..75470f64 100644 --- a/screen/wallets/list.js +++ b/screen/wallets/list.js @@ -41,6 +41,7 @@ export default class WalletsList extends Component { wallets: BlueApp.getWallets().concat(false), lastSnappedTo: 0, timeElpased: 0, + cameraPreviewIsPaused: true, }; EV(EV.enum.WALLETS_COUNT_CHANGED, () => this.redrawScreen(true)); @@ -300,7 +301,7 @@ export default class WalletsList extends Component { onSwiperIndexChanged = index => { StatusBar.setBarStyle(index === 1 ? 'dark-content' : 'light-content'); - this.setState({ cameraPreviewIsPaused: index === 1 }); + this.setState({ cameraPreviewIsPaused: index === 1 || index === undefined }); }; onBarScanned = value => { @@ -333,7 +334,7 @@ export default class WalletsList extends Component { { this.redrawScreen(); - this.setState({ cameraPreviewIsPaused: this.swiperRef.current.index === 1 }); + this.setState({ cameraPreviewIsPaused: this.swiperRef.current.index === 1 || this.swiperRef.current.index === undefined }); }} onWillBlur={() => this.setState({ cameraPreviewIsPaused: true })} /> From 4ed9d888e09cb3bdc6332a58e280e38dfc13037b Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Date: Wed, 8 Jan 2020 05:58:30 -0500 Subject: [PATCH 26/26] FIX: Crash on LNInvoice amount render --- screen/lnd/lndViewInvoice.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/screen/lnd/lndViewInvoice.js b/screen/lnd/lndViewInvoice.js index 9f2c2a85..0e2424b5 100644 --- a/screen/lnd/lndViewInvoice.js +++ b/screen/lnd/lndViewInvoice.js @@ -46,7 +46,7 @@ export default class LNDViewInvoice extends Component { BackHandler.addEventListener('hardwareBackPress', this.handleBackButton); } - async componentDidMount() { + componentDidMount() { this.fetchInvoiceInterval = setInterval(async () => { if (this.state.isFetchingInvoices) { try { @@ -277,11 +277,9 @@ export default class LNDViewInvoice extends Component { - {invoice && invoice.amt && ( - - {loc.lndViewInvoice.please_pay} {invoice.amt} {loc.lndViewInvoice.sats} - - )} + + {loc.lndViewInvoice.please_pay} {invoice.amt} {loc.lndViewInvoice.sats} + {invoice && invoice.hasOwnProperty('description') && invoice.description.length > 0 && ( {loc.lndViewInvoice.for} {invoice.description}