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 5a13c26f..1f25dc74 100644 --- a/BlueComponents.js +++ b/BlueComponents.js @@ -7,6 +7,7 @@ import { TouchableOpacity, TouchableWithoutFeedback, Animated, + Alert, ActivityIndicator, View, KeyboardAvoidingView, @@ -22,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'; @@ -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, @@ -733,15 +749,17 @@ export class BlueHeaderDefaultMain extends Component { } rightComponent={ - - - + this.props.onNewWalletPress && ( + + + + ) } /> @@ -942,7 +960,7 @@ export class BlueLoading extends Component { render() { return ( - + @@ -1405,19 +1423,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']) { @@ -1829,97 +1845,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 => { @@ -1953,7 +2038,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, }; @@ -1997,7 +2083,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/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/MainBottomTabs.js b/MainBottomTabs.js index 8f2b608a..cd3484b8 100644 --- a/MainBottomTabs.js +++ b/MainBottomTabs.js @@ -16,13 +16,13 @@ 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'; 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'; @@ -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: { @@ -210,6 +214,14 @@ const CreateWalletStackNavigator = createStackNavigator({ PleaseBackup: { screen: PleaseBackup, }, + PleaseBackupLNDHub: { + screen: PleaseBackupLNDHub, + swipeEnabled: false, + gesturesEnabled: false, + navigationOptions: { + header: null, + }, + }, }); const LightningScanInvoiceStackNavigator = createStackNavigator({ @@ -268,9 +280,6 @@ const MainBottomTabs = createStackNavigator( header: null, }, }, - ScanQrWif: { - screen: scanQrWif, - }, WalletExport: { screen: WalletExport, }, @@ -285,6 +294,7 @@ const MainBottomTabs = createStackNavigator( }, // SendDetails: { + routeName: 'SendDetails', screen: CreateTransactionStackNavigator, navigationOptions: { header: null, diff --git a/README.md b/README.md index 19a66f46..6e74e0d4 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) @@ -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: @@ -38,12 +43,27 @@ cd BlueWallet npm install ``` +Please make sure that your console is running the most stable versions of npm and node (even-numbered versions). + * 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: ``` 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/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"> 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" 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/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/class/app-storage.js b/class/app-storage.js index 16964c4e..36ff034a 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'); @@ -234,6 +235,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; @@ -342,6 +345,7 @@ export class AppStorage { for (let key of this.wallets) { if (typeof key === 'boolean') continue; if (typeof key === 'string') key = JSON.parse(key); + if (typeof key === 'boolean' || key.type === PlaceholderWallet.type) continue; if (key.prepareForSerialization) key.prepareForSerialization(); walletsToSave.push(JSON.stringify({ ...key, type: key.type })); } @@ -392,13 +396,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(); } } @@ -418,7 +422,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/deeplinkSchemaMatch.js b/class/deeplinkSchemaMatch.js new file mode 100644 index 00000000..194b0c44 --- /dev/null +++ b/class/deeplinkSchemaMatch.js @@ -0,0 +1,221 @@ +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) { + return { bitcoin, lndInvoice }; + } else { + return undefined; + } + } + return undefined; + } +} + +export default DeeplinkSchemaMatch; 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/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..0279404f --- /dev/null +++ b/class/walletImport.js @@ -0,0 +1,229 @@ +/* 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); + w.setUserHasSavedExport(true); + 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/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/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/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' }, diff --git a/package-lock.json b/package-lock.json index 1fc24ea3..10c1c0ac 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", @@ -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", @@ -11254,9 +11283,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 +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 d37a2b88..76619bf9 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", @@ -49,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", @@ -104,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", @@ -112,10 +116,11 @@ "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", - "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", 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/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} 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.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/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'); // 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; } } diff --git a/screen/send/details.js b/screen/send/details.js index 6f94ae17..5d69fd20 100644 --- a/screen/send/details.js +++ b/screen/send/details.js @@ -58,9 +58,14 @@ export default class SendDetails extends Component { title: loc.send.header, }); + state = { isLoading: true }; + constructor(props) { super(props); + this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this._keyboardDidShow); + this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this._keyboardDidHide); + let fromAddress; if (props.navigation.state.params) fromAddress = props.navigation.state.params.fromAddress; let fromSecret; @@ -177,9 +182,6 @@ export default class SendDetails extends Component { this.renderNavigationHeader(); console.log('send/details - componentDidMount'); StatusBar.setBarStyle('dark-content'); - this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this._keyboardDidShow); - this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this._keyboardDidHide); - let addresses = []; let initialMemo = ''; if (this.props.navigation.state.params.uri) { @@ -892,6 +894,7 @@ export default class SendDetails extends Component { address={item.address} isLoading={this.state.isLoading} 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/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/export.js b/screen/wallets/export.js index 5a2cd3d7..e2655f41 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'); @@ -47,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() { @@ -72,7 +79,11 @@ export default class WalletExport extends Component { return ( - + {this.state.wallet.typeReadable} @@ -80,9 +91,9 @@ export default class WalletExport extends Component { {(() => { if (this.state.wallet.getAddress()) { return ( - + {this.state.wallet.getAddress()} - + ); } })()} @@ -99,9 +110,12 @@ export default class WalletExport extends Component { /> - - {this.state.wallet.getSecret()} - + {this.state.wallet.type === LightningCustodianWallet.type ? ( + + ) : ( + {this.state.wallet.getSecret()} + )} + ); } diff --git a/screen/wallets/import.js b/screen/wallets/import.js index e952200a..86ecbe56 100644 --- a/screen/wallets/import.js +++ b/screen/wallets/import.js @@ -1,325 +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); - 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..75470f64 100644 --- a/screen/wallets/list.js +++ b/screen/wallets/list.js @@ -1,11 +1,27 @@ /* global alert */ import React, { Component } from 'react'; -import { View, TouchableOpacity, Text, FlatList, InteractionManager, RefreshControl, ScrollView } 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'; 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} */ @@ -14,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); @@ -39,8 +40,9 @@ export default class WalletsList extends Component { isFlatListRefreshControlHidden: true, wallets: BlueApp.getWallets().concat(false), lastSnappedTo: 0, + timeElpased: 0, + cameraPreviewIsPaused: true, }; - EV(EV.enum.WALLETS_COUNT_CHANGED, () => this.redrawScreen(true)); // here, when we receive TRANSACTIONS_COUNT_CHANGED we do not query @@ -70,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); } /** @@ -135,7 +144,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 +161,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 +209,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 +235,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,84 +292,168 @@ 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 }); } }; + onSwiperIndexChanged = index => { + StatusBar.setBarStyle(index === 1 ? 'dark-content' : 'light-content'); + this.setState({ cameraPreviewIsPaused: index === 1 || index === undefined }); + }; + + 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 || this.swiperRef.current.index === undefined }); }} + onWillBlur={() => this.setState({ cameraPreviewIsPaused: true })} /> this.refreshTransactions()} refreshing={!this.state.isFlatListRefreshControlHidden} /> + this.refreshTransactions()} + refreshing={!this.state.isFlatListRefreshControlHidden} + shouldRefresh={this.state.timeElpased} + /> } > - this.props.navigation.navigate('AddWallet')} /> - { - 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, }), }; 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; diff --git a/screen/wallets/reorderWallets.js b/screen/wallets/reorderWallets.js index e3bcd184..811075b9 100644 --- a/screen/wallets/reorderWallets.js +++ b/screen/wallets/reorderWallets.js @@ -4,7 +4,7 @@ import { SafeBlueArea, BlueNavigationStyle } from '../../BlueComponents'; import SortableList from 'react-native-sortable-list'; import LinearGradient from 'react-native-linear-gradient'; import PropTypes from 'prop-types'; -import { LightningCustodianWallet } from '../../class/lightning-custodian-wallet'; +import { PlaceholderWallet, LightningCustodianWallet } from '../../class'; import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; import WalletGradient from '../../class/walletGradient'; let EV = require('../../events'); @@ -51,7 +51,7 @@ export default class ReorderWallets extends Component { }, }); - const wallets = BlueApp.getWallets(); + const wallets = BlueApp.getWallets().filter(wallet => wallet.type !== PlaceholderWallet.type); this.setState({ data: wallets, isLoading: false, 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, - }), -}; 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; diff --git a/screen/wallets/transactions.js b/screen/wallets/transactions.js index 0a279498..1f849326 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'; @@ -81,11 +82,15 @@ export default class WalletTransactions extends Component { dataSource: this.getTransactions(15), limit: 15, pageSize: 20, + timeElapsed: 0, // this is to force a re-render for FlatList items. }; } componentDidMount() { this.props.navigation.setParams({ isLoading: false }); + this.interval = setInterval(() => { + this.setState(prev => ({ timeElapsed: prev.timeElapsed + 1 })); + }, 60000); } /** @@ -401,10 +406,17 @@ export default class WalletTransactions extends Component { componentWillUnmount() { this.onWillBlur(); + clearInterval(this.interval); } renderItem = item => { - return ; + return ( + + ); }; render() { @@ -433,7 +445,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(), + }), + }); + } + }} /> { 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 582e50fb..8c43060a 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,16 +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); -}); - -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.ok(hd.getTransactions().length === 153); + assert.ok(hd.getTransactions().length >= 3); }); it('Segwit HD (BIP49) can fetch more data if pointers to last_used_addr are lagging behind', async function() { @@ -143,9 +134,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 +205,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 +283,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 +384,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); } diff --git a/tests/integration/deepLinkSchemaMatch.test.js b/tests/integration/deepLinkSchemaMatch.test.js new file mode 100644 index 00000000..0c2d6194 --- /dev/null +++ b/tests/integration/deepLinkSchemaMatch.test.js @@ -0,0 +1,50 @@ +/* global describe, it */ +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, + }, + }); + }); + }); +});