Browse Source

Merge branch 'master' into settingsui

settingsui
Marcos Rodriguez 5 years ago
parent
commit
c40eb99bec
  1. 250
      App.js
  2. 106
      BlueComponents.js
  3. 4
      BlueElectrum.js
  4. 18
      MainBottomTabs.js
  5. 26
      README.md
  6. 1
      UnlockWith.js
  7. 1
      android/app/src/main/AndroidManifest.xml
  8. 1
      appcenter-post-build.sh
  9. 891
      class/abstract-hd-electrum-wallet.js
  10. 9
      class/abstract-wallet.js
  11. 12
      class/app-storage.js
  12. 221
      class/deeplinkSchemaMatch.js
  13. 885
      class/hd-segwit-bech32-wallet.js
  14. 201
      class/hd-segwit-p2sh-wallet.js
  15. 1
      class/index.js
  16. 31
      class/placeholder-wallet.js
  17. 4
      class/walletGradient.js
  18. 229
      class/walletImport.js
  19. 24
      ios/Podfile.lock
  20. 2
      loc/es.js
  21. 1
      models/fiatUnit.js
  22. 170
      package-lock.json
  23. 15
      package.json
  24. 33
      screen/lnd/lndCreateInvoice.js
  25. 4
      screen/lnd/lndViewInvoice.js
  26. 13
      screen/lnd/scanLndInvoice.js
  27. 69
      screen/receive/details.js
  28. 2
      screen/selftest.js
  29. 1
      screen/send/confirm.js
  30. 10
      screen/send/details.js
  31. 84
      screen/send/scanQrAddress.js
  32. 30
      screen/wallets/add.js
  33. 32
      screen/wallets/export.js
  34. 306
      screen/wallets/import.js
  35. 174
      screen/wallets/list.js
  36. 51
      screen/wallets/pleaseBackupLNDHub.js
  37. 4
      screen/wallets/reorderWallets.js
  38. 344
      screen/wallets/scanQrWif.js
  39. 84
      screen/wallets/selectWallet.js
  40. 33
      screen/wallets/transactions.js
  41. 7
      tests/integration/Electrum.test.js
  42. 41
      tests/integration/HDWallet.test.js
  43. 50
      tests/integration/deepLinkSchemaMatch.test.js

250
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 &&
!isAddressFromStoredWallet &&
this.state.clipboardContent !== clipboard &&
(this.isBitcoinAddress(clipboard) || this.isLightningInvoice(clipboard) || this.isLnUrl(clipboard))) ||
this.isBothBitcoinAndLightning(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 = () => {

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

4
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 = {};

18
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,

26
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)
<img src="https://i.imgur.com/hHYJnMj.png" width="100%">
## 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:
```

1
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();
}

1
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">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

1
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"

891
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<boolean>}
*/
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
}
}

9
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}

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

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

885
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 = [];
}
allowBatchSend() {
allowSend() {
return true;
}
allowSendMax(): boolean {
allowBatchSend() {
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() {
allowSendMax() {
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<boolean>}
*/
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
}
}

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

1
class/index.js

@ -12,3 +12,4 @@ export * from './lightning-custodian-wallet';
export * from './abstract-hd-wallet';
export * from './hd-segwit-bech32-wallet';
export * from './hd-segwit-bech32-transaction';
export * from './placeholder-wallet';

31
class/placeholder-wallet.js

@ -0,0 +1,31 @@
import { AbstractWallet } from './abstract-wallet';
export class PlaceholderWallet extends AbstractWallet {
static type = 'placeholder';
static typeReadable = 'Placeholder';
constructor() {
super();
this._isFailure = false;
}
allowSend() {
return false;
}
getLabel() {
return this.getIsFailure() ? 'Wallet Import' : 'Importing Wallet...';
}
allowReceive() {
return false;
}
getIsFailure() {
return this._isFailure;
}
setIsFailure(value) {
this._isFailure = value;
}
}

4
class/walletGradient.js

@ -5,6 +5,7 @@ import { HDLegacyBreadwalletWallet } from './hd-legacy-breadwallet-wallet';
import { HDLegacyP2PKHWallet } from './hd-legacy-p2pkh-wallet';
import { WatchOnlyWallet } from './watch-only-wallet';
import { HDSegwitBech32Wallet } from './hd-segwit-bech32-wallet';
import { PlaceholderWallet } from './placeholder-wallet';
export default class WalletGradient {
static hdSegwitP2SHWallet = ['#65ceef', '#68bbe1'];
@ -41,6 +42,9 @@ export default class WalletGradient {
case LightningCustodianWallet.type:
gradient = WalletGradient.lightningCustodianWallet;
break;
case PlaceholderWallet.type:
gradient = WalletGradient.watchOnlyWallet;
break;
case 'CreateWallet':
gradient = WalletGradient.createWallet;
break;

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

24
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

2
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í,',

1
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' },

170
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",

15
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",

33
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({

4
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 {
</View>
<BlueSpacing20 />
{invoice && invoice.amt && (
<BlueText>
{loc.lndViewInvoice.please_pay} {invoice.amt} {loc.lndViewInvoice.sats}
</BlueText>
)}
{invoice && invoice.hasOwnProperty('description') && invoice.description.length > 0 && (
<BlueText>
{loc.lndViewInvoice.for} {invoice.description}

13
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 <BlueLoading />;
}
return (
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
<SafeBlueArea forceInset={{ horizontal: 'always' }} style={{ flex: 1 }}>
@ -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}
/>
<View
style={{
@ -353,6 +357,7 @@ ScanLndInvoice.propTypes = {
getParam: PropTypes.func,
dismiss: PropTypes.func,
state: PropTypes.shape({
routeName: PropTypes.string,
params: PropTypes.shape({
uri: PropTypes.string,
fromSecret: PropTypes.string,

69
screen/receive/details.js

@ -13,6 +13,7 @@ import {
BlueBitcoinAmount,
BlueText,
BlueSpacing20,
BlueAlertWalletExportReminder,
} from '../../BlueComponents';
import PropTypes from 'prop-types';
import Privacy from '../../Privacy';
@ -45,64 +46,78 @@ export default class ReceiveDetails extends Component {
};
}
async componentDidMount() {
Privacy.enableBlur();
console.log('receive/details - componentDidMount');
{
renderReceiveDetails = async () => {
this.wallet.setUserHasSavedExport(true);
await BlueApp.saveToDisk();
let address;
let wallet;
for (let w of BlueApp.getWallets()) {
if (w.getSecret() === this.state.secret) {
// found our wallet
wallet = w;
}
}
if (wallet) {
if (wallet.getAddressAsync) {
if (wallet.chain === Chain.ONCHAIN) {
if (this.wallet.getAddressAsync) {
if (this.wallet.chain === Chain.ONCHAIN) {
try {
address = await Promise.race([wallet.getAddressAsync(), BlueApp.sleep(1000)]);
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 = wallet._getExternalAddressByIndex(wallet.next_free_address_index);
address = this.wallet._getExternalAddressByIndex(this.wallet.next_free_address_index);
} else {
BlueApp.saveToDisk(); // caching whatever getAddressAsync() generated internally
}
this.setState({
address: address,
});
} else if (wallet.chain === Chain.OFFCHAIN) {
} else if (this.wallet.chain === Chain.OFFCHAIN) {
try {
await Promise.race([wallet.getAddressAsync(), BlueApp.sleep(1000)]);
address = wallet.getAddress();
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 = wallet.getAddress();
address = this.wallet.getAddress();
} else {
BlueApp.saveToDisk(); // caching whatever getAddressAsync() generated internally
}
}
console.warn(address)
this.setState({
address: address,
});
} else if (wallet.getAddress) {
} else if (this.wallet.getAddress) {
this.setState({
address: wallet.getAddress(),
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() {

2
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');
//

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

10
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 && (
<BlueText style={{ alignSelf: 'flex-end', marginRight: 18, marginVertical: 8 }}>
@ -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,

84
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 (
<SafeBlueArea style={{ flex: 1 }}>
<View style={{ flex: 1, backgroundColor: '#000000' }}>
{!cameraPreviewIsPaused && !isLoading && (
<RNCamera
captureAudio={false}
androidCameraPermissionOptions={{
@ -33,11 +45,12 @@ export default class ScanQRCode extends React.Component {
buttonPositive: 'OK',
buttonNegative: 'Cancel',
}}
ref={ref => (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 && (
<TouchableOpacity
style={{
width: 40,
@ -49,10 +62,11 @@ export default class ScanQRCode extends React.Component {
right: 16,
top: 64,
}}
onPress={() => this.props.navigation.goBack(null)}
onPress={() => navigate(launchedBy)}
>
<Image style={{ alignSelf: 'center' }} source={require('../../img/close-white.png')} />
</TouchableOpacity>
)}
<TouchableOpacity
style={{
width: 40,
@ -65,7 +79,8 @@ export default class ScanQRCode extends React.Component {
bottom: 48,
}}
onPress={() => {
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);
},
);
}
}}
>
<Icon name="image" type="font-awesome" color="#0c2550" />
</TouchableOpacity>
</SafeBlueArea>
</View>
);
}
}
};
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;

30
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();
}
} else if (this.state.selectedIndex === 2) {
// zero index radio - HD segwit
w = new HDSegwitP2SHWallet();

32
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({
this.setState(
{
isLoading: false,
});
},
() => {
this.state.wallet.setUserHasSavedExport(true);
BlueApp.saveToDisk();
},
);
}
async componentWillUnmount() {
@ -72,7 +79,11 @@ export default class WalletExport extends Component {
return (
<SafeBlueArea style={{ flex: 1, paddingTop: 20 }}>
<View style={{ alignItems: 'center', flex: 1, justifyContent: 'center', paddingHorizontal: 0 }} onLayout={this.onLayout}>
<ScrollView
centerContent
contentContainerStyle={{ alignItems: 'center', justifyContent: 'center', flexGrow: 1 }}
onLayout={this.onLayout}
>
<View>
<BlueText>{this.state.wallet.typeReadable}</BlueText>
</View>
@ -80,9 +91,9 @@ export default class WalletExport extends Component {
{(() => {
if (this.state.wallet.getAddress()) {
return (
<View>
<BlueCard>
<BlueText>{this.state.wallet.getAddress()}</BlueText>
</View>
</BlueCard>
);
}
})()}
@ -99,9 +110,12 @@ export default class WalletExport extends Component {
/>
<BlueSpacing20 />
{this.state.wallet.type === LightningCustodianWallet.type ? (
<BlueCopyTextToClipboard text={this.state.wallet.getSecret()} />
) : (
<BlueText style={{ alignItems: 'center', paddingHorizontal: 8 }}>{this.state.wallet.getSecret()}</BlueText>
</View>
)}
</ScrollView>
</SafeBlueArea>
);
}

306
screen/wallets/import.js

@ -1,252 +1,54 @@
/* global alert */
import {
SegwitP2SHWallet,
LegacyWallet,
WatchOnlyWallet,
HDLegacyBreadwalletWallet,
HDSegwitP2SHWallet,
HDLegacyP2PKHWallet,
HDSegwitBech32Wallet,
} from '../../class';
import React, { Component } from 'react';
import React, { useEffect, useState } from 'react';
import { KeyboardAvoidingView, Platform, Dimensions, View, TouchableWithoutFeedback, Keyboard } from 'react-native';
import {
BlueFormMultiInput,
BlueButtonLink,
BlueFormLabel,
BlueLoading,
BlueDoneAndDismissKeyboardInputAccessory,
BlueButton,
SafeBlueArea,
BlueSpacing20,
BlueNavigationStyle,
} from '../../BlueComponents';
import PropTypes from 'prop-types';
import { LightningCustodianWallet } from '../../class/lightning-custodian-wallet';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import Privacy from '../../Privacy';
let EV = require('../../events');
let A = require('../../analytics');
/** @type {AppStorage} */
let BlueApp = require('../../BlueApp');
import { useNavigation, useNavigationParam } from 'react-navigation-hooks';
import WalletImport from '../../class/walletImport';
let loc = require('../../loc');
const { width } = Dimensions.get('window');
export default class WalletsImport extends Component {
static navigationOptions = {
...BlueNavigationStyle(),
title: loc.wallets.import.title,
};
constructor(props) {
super(props);
this.state = {
isLoading: true,
isToolbarVisibleForAndroid: false,
};
}
const WalletsImport = () => {
const [isToolbarVisibleForAndroid, setIsToolbarVisibleForAndroid] = useState(false);
const [importText, setImportText] = useState(useNavigationParam('label') || '');
const { navigate, dismiss } = useNavigation();
componentDidMount() {
this.setState({
isLoading: false,
label: '',
});
useEffect(() => {
Privacy.enableBlur();
}
componentWillUnmount() {
Privacy.disableBlur();
}
return () => Privacy.disableBlur();
});
async _saveWallet(w) {
if (BlueApp.getWallets().some(wallet => wallet.getSecret() === w.secret)) {
alert('This wallet has been previously imported.');
} else {
alert(loc.wallets.import.success);
ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false });
w.setLabel(loc.wallets.import.imported + ' ' + w.typeReadable);
BlueApp.wallets.push(w);
await BlueApp.saveToDisk();
EV(EV.enum.WALLETS_COUNT_CHANGED);
A(A.ENUM.CREATED_WALLET);
this.props.navigation.dismiss();
}
const importButtonPressed = () => {
if (importText.trim().length === 0) {
return;
}
importMnemonic(importText);
};
async importMnemonic(text) {
const importMnemonic = importText => {
try {
// is it lightning custodian?
if (text.indexOf('blitzhub://') !== -1 || text.indexOf('lndhub://') !== -1) {
let lnd = new LightningCustodianWallet();
if (text.includes('@')) {
const split = text.split('@');
lnd.setBaseURI(split[1]);
lnd.setSecret(split[0]);
} else {
lnd.setBaseURI(LightningCustodianWallet.defaultBaseUri);
lnd.setSecret(text);
}
lnd.init();
await lnd.authorize();
await lnd.fetchTransactions();
await lnd.fetchUserInvoices();
await lnd.fetchPendingTransactions();
await lnd.fetchBalance();
return this._saveWallet(lnd);
}
// trying other wallet types
let hd4 = new HDSegwitBech32Wallet();
hd4.setSecret(text);
if (hd4.validateMnemonic()) {
await hd4.fetchBalance();
if (hd4.getBalance() > 0) {
await hd4.fetchTransactions();
return this._saveWallet(hd4);
}
}
let segwitWallet = new SegwitP2SHWallet();
segwitWallet.setSecret(text);
if (segwitWallet.getAddress()) {
// ok its a valid WIF
let legacyWallet = new LegacyWallet();
legacyWallet.setSecret(text);
await legacyWallet.fetchBalance();
if (legacyWallet.getBalance() > 0) {
// yep, its legacy we're importing
await legacyWallet.fetchTransactions();
return this._saveWallet(legacyWallet);
} else {
// by default, we import wif as Segwit P2SH
await segwitWallet.fetchBalance();
await segwitWallet.fetchTransactions();
return this._saveWallet(segwitWallet);
}
}
// case - WIF is valid, just has uncompressed pubkey
let legacyWallet = new LegacyWallet();
legacyWallet.setSecret(text);
if (legacyWallet.getAddress()) {
await legacyWallet.fetchBalance();
await legacyWallet.fetchTransactions();
return this._saveWallet(legacyWallet);
}
// if we're here - nope, its not a valid WIF
let hd1 = new HDLegacyBreadwalletWallet();
hd1.setSecret(text);
if (hd1.validateMnemonic()) {
await hd1.fetchBalance();
if (hd1.getBalance() > 0) {
await hd1.fetchTransactions();
return this._saveWallet(hd1);
}
}
let hd2 = new HDSegwitP2SHWallet();
hd2.setSecret(text);
if (hd2.validateMnemonic()) {
await hd2.fetchBalance();
if (hd2.getBalance() > 0) {
await hd2.fetchTransactions();
return this._saveWallet(hd2);
}
}
let hd3 = new HDLegacyP2PKHWallet();
hd3.setSecret(text);
if (hd3.validateMnemonic()) {
await hd3.fetchBalance();
if (hd3.getBalance() > 0) {
await hd3.fetchTransactions();
return this._saveWallet(hd3);
}
}
// no balances? how about transactions count?
if (hd1.validateMnemonic()) {
await hd1.fetchTransactions();
if (hd1.getTransactions().length !== 0) {
return this._saveWallet(hd1);
}
}
if (hd2.validateMnemonic()) {
await hd2.fetchTransactions();
if (hd2.getTransactions().length !== 0) {
return this._saveWallet(hd2);
}
}
if (hd3.validateMnemonic()) {
await hd3.fetchTransactions();
if (hd3.getTransactions().length !== 0) {
return this._saveWallet(hd3);
}
}
if (hd4.validateMnemonic()) {
await hd4.fetchTransactions();
if (hd4.getTransactions().length !== 0) {
return this._saveWallet(hd4);
}
}
// is it even valid? if yes we will import as:
if (hd4.validateMnemonic()) {
return this._saveWallet(hd4);
}
// not valid? maybe its a watch-only address?
let watchOnly = new WatchOnlyWallet();
watchOnly.setSecret(text);
if (watchOnly.valid()) {
await watchOnly.fetchTransactions();
await watchOnly.fetchBalance();
return this._saveWallet(watchOnly);
}
// nope?
// TODO: try a raw private key
} catch (Err) {
console.warn(Err);
}
WalletImport.processImportText(importText);
dismiss();
} catch (error) {
alert(loc.wallets.import.error);
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
// Plan:
// 0. check if its HDSegwitBech32Wallet (BIP84)
// 1. check if its HDSegwitP2SHWallet (BIP49)
// 2. check if its HDLegacyP2PKHWallet (BIP44)
// 3. check if its HDLegacyBreadwalletWallet (no BIP, just "m/0")
// 4. check if its Segwit WIF (P2SH)
// 5. check if its Legacy WIF
// 6. check if its address (watch-only wallet)
// 7. check if its private key (segwit address P2SH) TODO
// 7. check if its private key (legacy address) TODO
}
setLabel(text) {
this.setState({
label: text,
}); /* also, a hack to make screen update new typed text */
}
};
render() {
if (this.state.isLoading) {
return (
<View style={{ flex: 1, paddingTop: 20 }}>
<BlueLoading />
</View>
);
}
const onBarScanned = value => {
setImportText(value);
importMnemonic(value);
};
return (
<SafeBlueArea forceInset={{ horizontal: 'always' }} style={{ flex: 1, paddingTop: 40 }}>
@ -255,27 +57,36 @@ export default class WalletsImport extends Component {
<BlueFormLabel>{loc.wallets.import.explanation}</BlueFormLabel>
<BlueSpacing20 />
<BlueFormMultiInput
value={this.state.label}
placeholder=""
value={importText}
contextMenuHidden
onChangeText={text => {
this.setLabel(text);
}}
onChangeText={setImportText}
inputAccessoryViewID={BlueDoneAndDismissKeyboardInputAccessory.InputAccessoryViewID}
onFocus={() => this.setState({ isToolbarVisibleForAndroid: true })}
onBlur={() => this.setState({ isToolbarVisibleForAndroid: false })}
onFocus={() => setIsToolbarVisibleForAndroid(true)}
onBlur={() => setIsToolbarVisibleForAndroid(false)}
/>
{Platform.select({
ios: (
<BlueDoneAndDismissKeyboardInputAccessory
onClearTapped={() => this.setState({ label: '' }, () => Keyboard.dismiss())}
onPasteTapped={text => this.setState({ label: text }, () => Keyboard.dismiss())}
onClearTapped={() => {
setImportText('');
Keyboard.dismiss();
}}
onPasteTapped={text => {
setImportText(text);
Keyboard.dismiss();
}}
/>
),
android: this.state.isToolbarVisibleForAndroid && (
android: isToolbarVisibleForAndroid && (
<BlueDoneAndDismissKeyboardInputAccessory
onClearTapped={() => this.setState({ label: '' }, () => Keyboard.dismiss())}
onPasteTapped={text => this.setState({ label: text }, () => Keyboard.dismiss())}
onClearTapped={() => {
setImportText('');
Keyboard.dismiss();
}}
onPasteTapped={text => {
setImportText(text);
Keyboard.dismiss();
}}
/>
),
})}
@ -289,37 +100,26 @@ export default class WalletsImport extends Component {
}}
>
<BlueButton
disabled={!this.state.label}
disabled={importText.trim().length === 0}
title={loc.wallets.import.do_import}
buttonStyle={{
width: width / 1.5,
}}
onPress={async () => {
if (!this.state.label) {
return;
}
this.setState({ isLoading: true }, async () => {
await this.importMnemonic(this.state.label.trim());
this.setState({ isLoading: false });
});
}}
onPress={importButtonPressed}
/>
<BlueButtonLink
title={loc.wallets.import.scan_qr}
onPress={() => {
this.props.navigation.navigate('ScanQrWif');
navigate('ScanQrAddress', { onBarScanned });
}}
/>
</View>
</SafeBlueArea>
);
}
}
};
WalletsImport.propTypes = {
navigation: PropTypes.shape({
navigate: PropTypes.func,
goBack: PropTypes.func,
dismiss: PropTypes.func,
}),
WalletsImport.navigationOptions = {
...BlueNavigationStyle(),
title: loc.wallets.import.title,
};
export default WalletsImport;

174
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: (
<TouchableOpacity
style={{ marginHorizontal: 16, width: 40, height: 40, justifyContent: 'center', alignItems: 'flex-end' }}
onPress={() => navigation.navigate('Settings')}
>
<Icon size={22} name="kebab-horizontal" type="octicon" color={BlueApp.settings.foregroundColor} />
</TouchableOpacity>
),
});
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,15 +161,44 @@ export default class WalletsList extends Component {
console.log('click', index);
let wallet = BlueApp.wallets[index];
if (wallet) {
if (wallet.type === PlaceholderWallet.type) {
Alert.alert(
loc.wallets.add.details,
'There was a problem importing this wallet.',
[
{
text: loc.wallets.details.delete,
onPress: () => {
WalletImport.removePlaceholderWallet();
EV(EV.enum.WALLETS_COUNT_CHANGED);
},
style: 'destructive',
},
{
text: 'Try Again',
onPress: () => {
this.props.navigation.navigate('ImportWallet', { label: wallet.getSecret() });
WalletImport.removePlaceholderWallet();
EV(EV.enum.WALLETS_COUNT_CHANGED);
},
style: 'default',
},
],
{ cancelable: false },
);
} else {
this.props.navigation.navigate('WalletTransactions', {
wallet: wallet,
key: `WalletTransactions-${wallet.getID()}`,
});
}
} else {
// if its out of index - this must be last card with incentive to create wallet
if (!BlueApp.getWallets().some(wallet => wallet.type === PlaceholderWallet.type)) {
this.props.navigation.navigate('AddWallet');
}
}
}
onSnapToItem(index) {
console.log('onSnapToItem', index);
@ -171,6 +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,33 +292,96 @@ 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 <BlueTransactionListItem item={data.item} itemPriceUnit={data.item.walletPreferredBalanceUnit} />;
};
renderNavigationHeader = () => {
return (
<View style={{ height: 44, alignItems: 'flex-end', justifyContent: 'center' }}>
<TouchableOpacity style={{ marginHorizontal: 16 }} onPress={() => this.props.navigation.navigate('Settings')}>
<Icon size={22} name="kebab-horizontal" type="octicon" color={BlueApp.settings.foregroundColor} />
</TouchableOpacity>
</View>
);
};
render() {
if (this.state.isLoading) {
return <BlueLoading />;
}
return (
<SafeBlueArea style={{ flex: 1, backgroundColor: '#FFFFFF' }}>
<View style={{ flex: 1, backgroundColor: '#000000' }}>
<NavigationEvents
onWillFocus={() => {
onDidFocus={() => {
this.redrawScreen();
this.setState({ cameraPreviewIsPaused: this.swiperRef.current.index === 1 || this.swiperRef.current.index === undefined });
}}
onWillBlur={() => this.setState({ cameraPreviewIsPaused: true })}
/>
<ScrollView
contentContainerStyle={{ flex: 1 }}
refreshControl={
<RefreshControl
onRefresh={() => this.refreshTransactions()}
refreshing={!this.state.isFlatListRefreshControlHidden}
shouldRefresh={this.state.timeElpased}
/>
}
>
<Swiper
style={styles.wrapper}
onIndexChanged={this.onSwiperIndexChanged}
index={1}
ref={this.swiperRef}
showsPagination={false}
showsButtons={false}
loop={false}
>
<View style={styles.scanQRWrapper}>
<ScanQRCode
cameraPreviewIsPaused={this.state.cameraPreviewIsPaused}
onBarScanned={this.onBarScanned}
showCloseButton={false}
initialCameraStatusReady={false}
launchedBy={this.props.navigation.state.routeName}
/>
</View>
<SafeBlueArea>
<View style={styles.walletsListWrapper}>
{this.renderNavigationHeader()}
<ScrollView
refreshControl={
<RefreshControl onRefresh={() => this.refreshTransactions()} refreshing={!this.state.isFlatListRefreshControlHidden} />
}
>
<BlueHeaderDefaultMain leftText={loc.wallets.list.title} onNewWalletPress={() => this.props.navigation.navigate('AddWallet')} />
<BlueHeaderDefaultMain
leftText={loc.wallets.list.title}
onNewWalletPress={
!BlueApp.getWallets().some(wallet => wallet.type === PlaceholderWallet.type)
? () => this.props.navigation.navigate('AddWallet')
: null
}
/>
<WalletsCarousel
removeClippedSubviews={false}
data={this.state.wallets}
@ -321,13 +426,34 @@ export default class WalletsList extends Component {
/>
</BlueList>
</ScrollView>
</View>
</SafeBlueArea>
</Swiper>
</ScrollView>
</View>
);
}
}
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,
}),
};

51
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 (
<SafeBlueArea style={{ flex: 1 }}>
<ScrollView centerContent contentContainerStyle={{ flexGrow: 1 }} onLayout={onLayout}>
<BlueCard>
<View>
<BlueTextCentered>
Please take a moment to save this LNDHub authentication. It's your backup you can use to restore the wallet on other device.
</BlueTextCentered>
</View>
<BlueSpacing20 />
<QRCode
value={wallet.secret}
logo={require('../../img/qr-code.png')}
logoSize={90}
size={qrCodeHeight}
color={BlueApp.settings.foregroundColor}
logoBackgroundColor={BlueApp.settings.brandingColor}
ecl={'H'}
/>
<BlueSpacing20 />
<BlueCopyTextToClipboard text={wallet.secret} />
<BlueSpacing20 />
<BlueButton onPress={navigation.dismiss} title="OK, I have saved it." />
</BlueCard>
</ScrollView>
</SafeBlueArea>
);
};
export default PleaseBackupLNDHub;

4
screen/wallets/reorderWallets.js

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

344
screen/wallets/scanQrWif.js

@ -1,344 +0,0 @@
/* global alert */
import React from 'react';
import { ActivityIndicator, Image, View, TouchableOpacity } from 'react-native';
import { BlueText, SafeBlueArea, BlueButton } from '../../BlueComponents';
import { RNCamera } from 'react-native-camera';
import { SegwitP2SHWallet, LegacyWallet, WatchOnlyWallet, HDLegacyP2PKHWallet, HDSegwitBech32Wallet } from '../../class';
import PropTypes from 'prop-types';
import { HDSegwitP2SHWallet } from '../../class/hd-segwit-p2sh-wallet';
import { LightningCustodianWallet } from '../../class/lightning-custodian-wallet';
import bip21 from 'bip21';
/** @type {AppStorage} */
let BlueApp = require('../../BlueApp');
let EV = require('../../events');
let bip38 = require('../../bip38');
let wif = require('wif');
let prompt = require('../../prompt');
let loc = require('../../loc');
export default class ScanQrWif extends React.Component {
static navigationOptions = {
header: null,
};
state = { isLoading: false };
onBarCodeScanned = async ret => {
if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.pausePreview();
if (+new Date() - this.lastTimeIveBeenHere < 6000) {
this.lastTimeIveBeenHere = +new Date();
return;
}
this.lastTimeIveBeenHere = +new Date();
this.setState({ isLoading: true });
if (ret.data[0] === '6') {
// password-encrypted, need to ask for password and decrypt
console.log('trying to decrypt...');
this.setState({
message: loc.wallets.scanQrWif.decoding,
});
shold_stop_bip38 = undefined; // eslint-disable-line
let password = await prompt(loc.wallets.scanQrWif.input_password, loc.wallets.scanQrWif.password_explain);
if (!password) {
return;
}
let that = this;
try {
let decryptedKey = await bip38.decrypt(ret.data, password, function(status) {
that.setState({
message: loc.wallets.scanQrWif.decoding + '... ' + status.percent.toString().substr(0, 4) + ' %',
});
});
ret.data = wif.encode(0x80, decryptedKey.privateKey, decryptedKey.compressed);
} catch (e) {
console.log(e.message);
if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.resumePreview();
this.setState({ message: false, isLoading: false });
return alert(loc.wallets.scanQrWif.bad_password);
}
this.setState({ message: false, isLoading: false });
}
for (let w of BlueApp.wallets) {
if (w.getSecret() === ret.data) {
// lookig for duplicates
this.setState({ isLoading: false });
if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.resumePreview();
return alert(loc.wallets.scanQrWif.wallet_already_exists); // duplicate, not adding
}
}
// is it HD BIP49 mnemonic?
let hd = new HDSegwitP2SHWallet();
hd.setSecret(ret.data);
if (hd.validateMnemonic()) {
for (let w of BlueApp.wallets) {
if (w.getSecret() === hd.getSecret()) {
// lookig for duplicates
this.setState({ isLoading: false });
if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.resumePreview();
return alert(loc.wallets.scanQrWif.wallet_already_exists); // duplicate, not adding
}
}
this.setState({ isLoading: true });
hd.setLabel(loc.wallets.import.imported + ' ' + hd.typeReadable);
await hd.fetchBalance();
if (hd.getBalance() !== 0) {
await hd.fetchTransactions();
BlueApp.wallets.push(hd);
await BlueApp.saveToDisk();
alert(loc.wallets.import.success);
this.props.navigation.popToTop();
setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500);
this.setState({ isLoading: false });
return;
}
}
// nope
// is it HD legacy (BIP44) mnemonic?
hd = new HDLegacyP2PKHWallet();
hd.setSecret(ret.data);
if (hd.validateMnemonic()) {
for (let w of BlueApp.wallets) {
if (w.getSecret() === hd.getSecret()) {
// lookig for duplicates
this.setState({ isLoading: false });
if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.resumePreview();
return alert(loc.wallets.scanQrWif.wallet_already_exists); // duplicate, not adding
}
}
await hd.fetchTransactions();
if (hd.getTransactions().length !== 0) {
await hd.fetchBalance();
hd.setLabel(loc.wallets.import.imported + ' ' + hd.typeReadable);
BlueApp.wallets.push(hd);
await BlueApp.saveToDisk();
alert(loc.wallets.import.success);
this.props.navigation.popToTop();
setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500);
this.setState({ isLoading: false });
return;
}
}
// nope
// is it HD BIP49 mnemonic?
hd = new HDSegwitBech32Wallet();
hd.setSecret(ret.data);
if (hd.validateMnemonic()) {
for (let w of BlueApp.wallets) {
if (w.getSecret() === hd.getSecret()) {
// lookig for duplicates
this.setState({ isLoading: false });
if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.resumePreview();
return alert(loc.wallets.scanQrWif.wallet_already_exists); // duplicate, not adding
}
}
this.setState({ isLoading: true });
hd.setLabel(loc.wallets.import.imported + ' ' + hd.typeReadable);
BlueApp.wallets.push(hd);
await hd.fetchBalance();
await hd.fetchTransactions();
await BlueApp.saveToDisk();
alert(loc.wallets.import.success);
this.props.navigation.popToTop();
setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500);
this.setState({ isLoading: false });
return;
}
// nope
// is it lndhub?
if (ret.data.indexOf('blitzhub://') !== -1 || ret.data.indexOf('lndhub://') !== -1) {
this.setState({ isLoading: true });
let lnd = new LightningCustodianWallet();
lnd.setSecret(ret.data);
if (ret.data.includes('@')) {
const split = ret.data.split('@');
lnd.setBaseURI(split[1]);
lnd.init();
lnd.setSecret(split[0]);
}
try {
await lnd.authorize();
await lnd.fetchTransactions();
await lnd.fetchUserInvoices();
await lnd.fetchPendingTransactions();
await lnd.fetchBalance();
} catch (Err) {
console.log(Err);
this.setState({ isLoading: false });
if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.resumePreview();
alert(Err.message);
return;
}
BlueApp.wallets.push(lnd);
lnd.setLabel(loc.wallets.import.imported + ' ' + lnd.typeReadable);
this.props.navigation.popToTop();
alert(loc.wallets.import.success);
await BlueApp.saveToDisk();
setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500);
this.setState({ isLoading: false });
return;
}
// nope
// is it just address..?
let watchOnly = new WatchOnlyWallet();
let watchAddr = ret.data;
// Is it BIP21 format?
if (ret.data.indexOf('bitcoin:') === 0 || ret.data.indexOf('BITCOIN:') === 0) {
try {
watchAddr = bip21.decode(ret.data).address;
} catch (err) {
console.log(err);
}
}
if (watchOnly.setSecret(watchAddr) && watchOnly.valid()) {
watchOnly.setLabel(loc.wallets.scanQrWif.imported_watchonly);
BlueApp.wallets.push(watchOnly);
alert(loc.wallets.scanQrWif.imported_watchonly + loc.wallets.scanQrWif.with_address + watchOnly.getAddress());
await watchOnly.fetchBalance();
await watchOnly.fetchTransactions();
await BlueApp.saveToDisk();
this.props.navigation.popToTop();
setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500);
this.setState({ isLoading: false });
return;
}
// nope
let newWallet = new SegwitP2SHWallet();
newWallet.setSecret(ret.data);
let newLegacyWallet = new LegacyWallet();
newLegacyWallet.setSecret(ret.data);
if (newWallet.getAddress() === false && newLegacyWallet.getAddress() === false) {
alert(loc.wallets.scanQrWif.bad_wif);
if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.resumePreview();
this.setState({ isLoading: false });
return;
}
if (newWallet.getAddress() === false && newLegacyWallet.getAddress() !== false) {
// case - WIF is valid, just has uncompressed pubkey
newLegacyWallet.setLabel(loc.wallets.scanQrWif.imported_legacy);
BlueApp.wallets.push(newLegacyWallet);
alert(loc.wallets.scanQrWif.imported_wif + ret.data + loc.wallets.scanQrWif.with_address + newLegacyWallet.getAddress());
await newLegacyWallet.fetchBalance();
await newLegacyWallet.fetchTransactions();
await BlueApp.saveToDisk();
this.props.navigation.popToTop();
setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500);
return;
}
this.setState({ isLoading: true });
await newLegacyWallet.fetchBalance();
console.log('newLegacyWallet == ', newLegacyWallet.getBalance());
if (newLegacyWallet.getBalance()) {
newLegacyWallet.setLabel(loc.wallets.scanQrWif.imported_legacy);
BlueApp.wallets.push(newLegacyWallet);
alert(loc.wallets.scanQrWif.imported_wif + ret.data + loc.wallets.scanQrWif.with_address + newLegacyWallet.getAddress());
await newLegacyWallet.fetchTransactions();
} else {
await newWallet.fetchBalance();
await newWallet.fetchTransactions();
newWallet.setLabel(loc.wallets.scanQrWif.imported_segwit);
BlueApp.wallets.push(newWallet);
alert(loc.wallets.scanQrWif.imported_wif + ret.data + loc.wallets.scanQrWif.with_address + newWallet.getAddress());
}
await BlueApp.saveToDisk();
this.props.navigation.popToTop();
setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500);
}; // end
render() {
if (this.state.isLoading) {
return (
<View style={{ flex: 1, paddingTop: 20, justifyContent: 'center', alignContent: 'center' }}>
<ActivityIndicator />
</View>
);
}
return (
<View style={{ flex: 1 }}>
{(() => {
if (this.state.message) {
return (
<SafeBlueArea>
<View
style={{
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
}}
>
<BlueText>{this.state.message}</BlueText>
<BlueButton
icon={{ name: 'ban', type: 'font-awesome' }}
onPress={async () => {
this.setState({ message: false });
shold_stop_bip38 = true; // eslint-disable-line
}}
title={loc.wallets.scanQrWif.cancel}
/>
</View>
</SafeBlueArea>
);
} else {
return (
<SafeBlueArea style={{ flex: 1 }}>
<RNCamera
captureAudio={false}
androidCameraPermissionOptions={{
title: 'Permission to use camera',
message: 'We need your permission to use your camera',
buttonPositive: 'OK',
buttonNegative: 'Cancel',
}}
style={{ flex: 1, justifyContent: 'space-between' }}
onBarCodeRead={this.onBarCodeScanned}
ref={ref => (this.cameraRef = ref)}
barCodeTypes={[RNCamera.Constants.BarCodeType.qr]}
/>
<TouchableOpacity
style={{
width: 40,
height: 40,
marginLeft: 24,
backgroundColor: '#FFFFFF',
justifyContent: 'center',
borderRadius: 20,
position: 'absolute',
top: 64,
}}
onPress={() => this.props.navigation.goBack(null)}
>
<Image style={{ alignSelf: 'center' }} source={require('../../img/close.png')} />
</TouchableOpacity>
</SafeBlueArea>
);
}
})()}
</View>
);
}
}
ScanQrWif.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.func,
popToTop: PropTypes.func,
navigate: PropTypes.func,
}),
};

84
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 (
<TouchableOpacity
onPress={() => {
ReactNativeHapticFeedback.trigger('selection', { ignoreAndroidSystemSettings: false });
this.props.navigation.getParam('onWalletSelect')(item);
onWalletSelect(item);
}}
>
<View
@ -132,14 +116,13 @@ export default class SelectWallet extends Component {
);
};
render() {
if (this.state.isLoading) {
if (isLoading) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignContent: 'center', paddingTop: 20 }}>
<ActivityIndicator />
</View>
);
} else if (this.state.data.length <= 0) {
} else if (data.length <= 0) {
return (
<SafeBlueArea style={{ flex: 1 }}>
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', paddingTop: 20 }}>
@ -151,27 +134,18 @@ export default class SelectWallet extends Component {
</View>
</SafeBlueArea>
);
}
} else {
return (
<SafeBlueArea>
<FlatList
style={{ flex: 1 }}
extraData={this.state.data}
data={this.state.data}
renderItem={this._renderItem}
keyExtractor={(_item, index) => `${index}`}
/>
<SafeBlueArea style={{ flex: 1 }}>
<FlatList extraData={data} data={data} renderItem={renderItem} keyExtractor={(_item, index) => `${index}`} />
</SafeBlueArea>
);
}
}
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;

33
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 <BlueTransactionListItem item={item.item} itemPriceUnit={this.state.wallet.getPreferredBalanceUnit()} />;
return (
<BlueTransactionListItem
item={item.item}
itemPriceUnit={this.state.wallet.getPreferredBalanceUnit()}
shouldRefresh={this.state.timeElapsed}
/>
);
};
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(),
}),
});
}
}}
/>
<View style={{ backgroundColor: '#FFFFFF', flex: 1 }}>
<FlatList

7
tests/integration/Electrum.test.js

File diff suppressed because one or more lines are too long

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

50
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,
},
});
});
});
});
Loading…
Cancel
Save