ADD: Export/Import PSBTs

eipiji
This commit is contained in:
Marcos Rodriguez 2019-12-31 21:31:04 -06:00
parent f5f5e787b8
commit 9e302df75d
22 changed files with 971 additions and 414 deletions

252
App.js
View file

@ -1,18 +1,17 @@
import React from 'react';
import { Linking, DeviceEventEmitter, AppState, Clipboard, StyleSheet, KeyboardAvoidingView, Platform, View } from 'react-native';
import AsyncStorage from '@react-native-community/async-storage';
import Modal from 'react-native-modal';
import { NavigationActions } from 'react-navigation';
import MainBottomTabs from './MainBottomTabs';
import NavigationService from './NavigationService';
import { BlueTextCentered, BlueButton } from './BlueComponents';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import url from 'url';
import { AppStorage, LightningCustodianWallet } from './class';
import { Chain } from './models/bitcoinUnits';
import QuickActions from 'react-native-quick-actions';
import * as Sentry from '@sentry/react-native';
import OnAppLaunch from './class/onAppLaunch';
import DeeplinkSchemaMatch from './class/deeplinkSchemaMatch';
import BitcoinBIP70TransactionDecode from './bip70/bip70';
const A = require('./analytics');
if (process.env.NODE_ENV !== 'development') {
@ -21,11 +20,9 @@ if (process.env.NODE_ENV !== 'development') {
});
}
const bitcoin = require('bitcoinjs-lib');
const bitcoinModalString = 'Bitcoin address';
const lightningModalString = 'Lightning Invoice';
const loc = require('./loc');
/** @type {AppStorage} */
const BlueApp = require('./BlueApp');
export default class App extends React.Component {
@ -62,7 +59,7 @@ export default class App extends React.Component {
} else {
const url = await Linking.getInitialURL();
if (url) {
if (this.hasSchema(url)) {
if (DeeplinkSchemaMatch.hasSchema(url)) {
this.handleOpenURL({ url });
}
} else {
@ -116,12 +113,23 @@ export default class App extends React.Component {
const isAddressFromStoredWallet = BlueApp.getWallets().some(wallet =>
wallet.chain === Chain.ONCHAIN ? wallet.weOwnAddress(clipboard) : wallet.isInvoiceGeneratedByWallet(clipboard),
);
const isBitcoinAddress =
DeeplinkSchemaMatch.isBitcoinAddress(clipboard) || BitcoinBIP70TransactionDecode.matchesPaymentURL(clipboard);
const isLightningInvoice = DeeplinkSchemaMatch.isLightningInvoice(clipboard);
const isLNURL = DeeplinkSchemaMatch.isLnUrl(clipboard);
const isBothBitcoinAndLightning = DeeplinkSchemaMatch.isBothBitcoinAndLightning(clipboard);
if (
(!isAddressFromStoredWallet &&
this.state.clipboardContent !== clipboard &&
(this.isBitcoinAddress(clipboard) || this.isLightningInvoice(clipboard) || this.isLnUrl(clipboard))) ||
this.isBothBitcoinAndLightning(clipboard)
!isAddressFromStoredWallet &&
this.state.clipboardContent !== clipboard &&
(isBitcoinAddress || isLightningInvoice || isLNURL || isBothBitcoinAndLightning)
) {
if (isBitcoinAddress) {
this.setState({ clipboardContentModalAddressType: bitcoinModalString });
} else if (isLightningInvoice || isLNURL) {
this.setState({ clipboardContentModalAddressType: lightningModalString });
} else if (isBothBitcoinAndLightning) {
this.setState({ clipboardContentModalAddressType: bitcoinModalString });
}
this.setState({ isClipboardContentModalVisible: true });
}
this.setState({ clipboardContent: clipboard });
@ -130,94 +138,6 @@ export default class App extends React.Component {
}
};
hasSchema(schemaString) {
if (typeof schemaString !== 'string' || schemaString.length <= 0) return false;
const lowercaseString = schemaString.trim().toLowerCase();
return (
lowercaseString.startsWith('bitcoin:') ||
lowercaseString.startsWith('lightning:') ||
lowercaseString.startsWith('blue:') ||
lowercaseString.startsWith('bluewallet:') ||
lowercaseString.startsWith('lapp:')
);
}
isBitcoinAddress(address) {
address = address
.replace('bitcoin:', '')
.replace('bitcoin=', '')
.split('?')[0];
let isValidBitcoinAddress = false;
try {
bitcoin.address.toOutputScript(address);
isValidBitcoinAddress = true;
this.setState({ clipboardContentModalAddressType: bitcoinModalString });
} catch (err) {
isValidBitcoinAddress = false;
}
return isValidBitcoinAddress;
}
isLightningInvoice(invoice) {
let isValidLightningInvoice = false;
if (invoice.toLowerCase().startsWith('lightning:lnb') || invoice.toLowerCase().startsWith('lnb')) {
this.setState({ clipboardContentModalAddressType: lightningModalString });
isValidLightningInvoice = true;
}
return isValidLightningInvoice;
}
isLnUrl(text) {
if (text.toLowerCase().startsWith('lightning:lnurl') || text.toLowerCase().startsWith('lnurl')) {
return true;
}
return false;
}
isBothBitcoinAndLightning(url) {
if (url.includes('lightning') && url.includes('bitcoin')) {
const txInfo = url.split(/(bitcoin:|lightning:|lightning=|bitcoin=)+/);
let bitcoin;
let lndInvoice;
for (const [index, value] of txInfo.entries()) {
try {
// Inside try-catch. We dont wan't to crash in case of an out-of-bounds error.
if (value.startsWith('bitcoin')) {
bitcoin = `bitcoin:${txInfo[index + 1]}`;
if (!this.isBitcoinAddress(bitcoin)) {
bitcoin = false;
break;
}
} else if (value.startsWith('lightning')) {
lndInvoice = `lightning:${txInfo[index + 1]}`;
if (!this.isLightningInvoice(lndInvoice)) {
lndInvoice = false;
break;
}
}
} catch (e) {
console.log(e);
}
if (bitcoin && lndInvoice) break;
}
if (bitcoin && lndInvoice) {
this.setState({
clipboardContent: { bitcoin, lndInvoice },
});
return { bitcoin, lndInvoice };
} else {
return undefined;
}
}
return undefined;
}
isSafelloRedirect(event) {
let urlObject = url.parse(event.url, true) // eslint-disable-line
return !!urlObject.query['safello-state-token'];
}
isBothBitcoinAndLightningWalletSelect = wallet => {
const clipboardContent = this.state.clipboardContent;
if (wallet.chain === Chain.ONCHAIN) {
@ -246,141 +166,7 @@ export default class App extends React.Component {
};
handleOpenURL = event => {
if (event.url === null) {
return;
}
if (typeof event.url !== 'string') {
return;
}
let isBothBitcoinAndLightning;
try {
isBothBitcoinAndLightning = this.isBothBitcoinAndLightning(event.url);
} catch (e) {
console.log(e);
}
if (isBothBitcoinAndLightning) {
this.navigator &&
this.navigator.dispatch(
NavigationActions.navigate({
routeName: 'HandleOffchainAndOnChain',
params: {
onWalletSelect: this.isBothBitcoinAndLightningWalletSelect,
},
}),
);
} else if (this.isBitcoinAddress(event.url)) {
this.navigator &&
this.navigator.dispatch(
NavigationActions.navigate({
routeName: 'SendDetails',
params: {
uri: event.url,
},
}),
);
} else if (this.isLightningInvoice(event.url)) {
this.navigator &&
this.navigator.dispatch(
NavigationActions.navigate({
routeName: 'ScanLndInvoice',
params: {
uri: event.url,
},
}),
);
} else if (this.isLnUrl(event.url)) {
this.navigator &&
this.navigator.dispatch(
NavigationActions.navigate({
routeName: 'LNDCreateInvoice',
params: {
uri: event.url,
},
}),
);
} else if (this.isSafelloRedirect(event)) {
let urlObject = url.parse(event.url, true) // eslint-disable-line
const safelloStateToken = urlObject.query['safello-state-token'];
this.navigator &&
this.navigator.dispatch(
NavigationActions.navigate({
routeName: 'BuyBitcoin',
params: {
uri: event.url,
safelloStateToken,
},
}),
);
} else {
let urlObject = url.parse(event.url, true); // eslint-disable-line
console.log('parsed', urlObject);
(async () => {
if (urlObject.protocol === 'bluewallet:' || urlObject.protocol === 'lapp:' || urlObject.protocol === 'blue:') {
switch (urlObject.host) {
case 'openlappbrowser':
console.log('opening LAPP', urlObject.query.url);
// searching for LN wallet:
let haveLnWallet = false;
for (let w of BlueApp.getWallets()) {
if (w.type === LightningCustodianWallet.type) {
haveLnWallet = true;
}
}
if (!haveLnWallet) {
// need to create one
let w = new LightningCustodianWallet();
w.setLabel(this.state.label || w.typeReadable);
try {
let lndhub = await AsyncStorage.getItem(AppStorage.LNDHUB);
if (lndhub) {
w.setBaseURI(lndhub);
w.init();
}
await w.createAccount();
await w.authorize();
} catch (Err) {
// giving up, not doing anything
return;
}
BlueApp.wallets.push(w);
await BlueApp.saveToDisk();
}
// now, opening lapp browser and navigating it to URL.
// looking for a LN wallet:
let lnWallet;
for (let w of BlueApp.getWallets()) {
if (w.type === LightningCustodianWallet.type) {
lnWallet = w;
break;
}
}
if (!lnWallet) {
// something went wrong
return;
}
this.navigator &&
this.navigator.dispatch(
NavigationActions.navigate({
routeName: 'LappBrowser',
params: {
fromSecret: lnWallet.getSecret(),
fromWallet: lnWallet,
url: urlObject.query.url,
},
}),
);
break;
}
}
})();
}
DeeplinkSchemaMatch.navigationRouteFor(event, value => this.navigator && this.navigator.dispatch(NavigationActions.navigate(value)));
};
renderClipboardContentModal = () => {

View file

@ -1381,7 +1381,7 @@ export class NewWalletPanel extends Component {
style={{
padding: 15,
borderRadius: 10,
minHeight: 164,
minHeight: Platform.OS === 'ios' ? 164 : 181,
justifyContent: 'center',
alignItems: 'center',
}}
@ -1837,7 +1837,9 @@ export class WalletsCarousel extends Component {
<NewWalletPanel
onPress={() => {
if (WalletsCarousel.handleClick) {
this.onPressedOut();
WalletsCarousel.handleClick(index);
this.onPressedOut();
}
}}
/>
@ -1857,7 +1859,9 @@ export class WalletsCarousel extends Component {
onPressOut={item.getIsFailure() ? this.onPressedOut : null}
onPress={() => {
if (item.getIsFailure() && WalletsCarousel.handleClick) {
this.onPressedOut();
WalletsCarousel.handleClick(index);
this.onPressedOut();
}
}}
>
@ -1925,7 +1929,9 @@ export class WalletsCarousel extends Component {
onLongPress={WalletsCarousel.handleLongPress}
onPress={() => {
if (WalletsCarousel.handleClick) {
this.onPressedOut();
WalletsCarousel.handleClick(index);
this.onPressedOut();
}
}}
>
@ -2037,7 +2043,8 @@ export class BlueAddressInput extends Component {
static propTypes = {
isLoading: PropTypes.bool,
onChangeText: PropTypes.func,
onBarScanned: PropTypes.func,
onBarScanned: PropTypes.func.isRequired,
launchedBy: PropTypes.string.isRequired,
address: PropTypes.string,
placeholder: PropTypes.string,
};
@ -2081,7 +2088,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={{

View file

@ -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: {
@ -206,6 +210,7 @@ const CreateWalletStackNavigator = createStackNavigator({
},
ImportWallet: {
screen: ImportWallet,
routeName: 'ImportWallet',
},
PleaseBackup: {
screen: PleaseBackup,
@ -290,6 +295,7 @@ const MainBottomTabs = createStackNavigator(
},
//
SendDetails: {
routeName: 'SendDetails',
screen: CreateTransactionStackNavigator,
navigationOptions: {
header: null,

View file

@ -4,7 +4,8 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:name=".MainApplication"
android:label="@string/app_name"
@ -34,6 +35,26 @@
<data android:scheme="lapp" />
<data android:scheme="blue" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.EDIT" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:mimeType="application/octet-stream"
android:host="*"
android:pathPattern=".*\\.psbt"
/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.EDIT" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:mimeType="text/plain"
android:host="*"
android:pathPattern=".*\\.psbt"
/>
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
</application>

View file

@ -8,6 +8,7 @@ const BlueElectrum = require('../BlueElectrum');
const HDNode = require('bip32');
const coinSelectAccumulative = require('coinselect/accumulative');
const coinSelectSplit = require('coinselect/split');
const reverse = require('buffer-reverse');
const { RNRandomBytes } = NativeModules;
@ -719,7 +720,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
* @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) {
createTransaction(utxos, targets, feeRate, changeAddress, sequence, skipSigning = false, masterFingerprint) {
if (!changeAddress) throw new Error('No change address provided');
sequence = sequence || AbstractHDElectrumWallet.defaultRBFSequence;
@ -756,7 +757,13 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
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]);
let masterFingerprintBuffer;
if (masterFingerprint) {
const hexBuffer = Buffer.from(Number(masterFingerprint).toString(16), 'hex');
masterFingerprintBuffer = Buffer.from(reverse(hexBuffer));
} else {
masterFingerprintBuffer = 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);
@ -767,7 +774,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
sequence,
bip32Derivation: [
{
masterFingerprint,
masterFingerprint: masterFingerprintBuffer,
path,
pubkey,
},
@ -789,7 +796,15 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
let path = this._getDerivationPathByAddress(output.address);
let pubkey = this._getPubkeyByAddress(output.address);
let masterFingerprint = Buffer.from([0x00, 0x00, 0x00, 0x00]);
let masterFingerprintBuffer;
if (masterFingerprint) {
const hexBuffer = Buffer.from(Number(masterFingerprint).toString(16), 'hex')
masterFingerprintBuffer = Buffer.from(reverse(hexBuffer));
} else {
masterFingerprintBuffer = 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
@ -801,7 +816,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
if (change) {
outputData['bip32Derivation'] = [
{
masterFingerprint,
masterFingerprint: masterFingerprintBuffer,
path,
pubkey,
},

View file

@ -305,7 +305,6 @@ export class AppStorage {
if (key.prepareForSerialization) key.prepareForSerialization();
walletsToSave.push(JSON.stringify({ ...key, type: key.type }));
}
let data = {
wallets: walletsToSave,
tx_metadata: this.tx_metadata,

View file

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

View file

@ -18,24 +18,34 @@ const BlueApp = require('../BlueApp');
const loc = require('../loc');
export default class WalletImport {
static async _saveWallet(w) {
static async _saveWallet(w, additionalProperties) {
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);
if (additionalProperties) {
for (const [key, value] of Object.entries(additionalProperties)) {
w[key] = value;
}
}
WalletImport.removePlaceholderWallet();
BlueApp.wallets.push(w);
await BlueApp.saveToDisk();
A(A.ENUM.CREATED_WALLET);
alert(loc.wallets.import.success);
}
EV(EV.enum.WALLETS_COUNT_CHANGED);
} catch (_e) {}
} catch (e) {
alert(e);
console.log(e);
WalletImport.removePlaceholderWallet();
EV(EV.enum.WALLETS_COUNT_CHANGED);
}
}
static removePlaceholderWallet() {
@ -58,7 +68,7 @@ export default class WalletImport {
return BlueApp.getWallets().some(wallet => wallet.type === PlaceholderWallet.type);
}
static async processImportText(importText) {
static async processImportText(importText, additionalProperties) {
if (WalletImport.isCurrentlyImportingWallet()) {
return;
}
@ -209,7 +219,7 @@ export default class WalletImport {
if (watchOnly.valid()) {
await watchOnly.fetchTransactions();
await watchOnly.fetchBalance();
return WalletImport._saveWallet(watchOnly);
return WalletImport._saveWallet(watchOnly, additionalProperties);
}
// nope?

View file

@ -11,6 +11,7 @@ export class WatchOnlyWallet extends LegacyWallet {
constructor() {
super();
this.use_with_hardware_wallet = false;
this.masterFingerprint = false;
}
allowSend() {
@ -146,7 +147,7 @@ export class WatchOnlyWallet extends LegacyWallet {
*/
createTransaction(utxos, targets, feeRate, changeAddress, sequence) {
if (this._hdWalletInstance instanceof HDSegwitBech32Wallet) {
return this._hdWalletInstance.createTransaction(utxos, targets, feeRate, changeAddress, sequence, true);
return this._hdWalletInstance.createTransaction(utxos, targets, feeRate, changeAddress, sequence, true, this.masterFingerprint);
} else {
throw new Error('Not a zpub watch-only wallet, cant create PSBT (or just not initialized)');
}

View file

@ -164,6 +164,7 @@
3271B0BA236E329400DA766F /* API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = "<group>"; };
32B5A3282334450100F8D608 /* BlueWallet-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "BlueWallet-Bridging-Header.h"; sourceTree = "<group>"; };
32B5A3292334450100F8D608 /* Bridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bridge.swift; sourceTree = "<group>"; };
32C7944323B8879D00BE2AFA /* BlueWalletRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = BlueWalletRelease.entitlements; path = BlueWallet/BlueWalletRelease.entitlements; sourceTree = "<group>"; };
32F0A24F2310B0700095C559 /* BlueWalletWatch Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "BlueWalletWatch Extension.entitlements"; sourceTree = "<group>"; };
32F0A2502310B0910095C559 /* BlueWallet.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = BlueWallet.entitlements; path = BlueWallet/BlueWallet.entitlements; sourceTree = "<group>"; };
32F0A2992311DBB20095C559 /* ComplicationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComplicationController.swift; sourceTree = "<group>"; };
@ -332,6 +333,7 @@
13B07FAE1A68108700A75B9A /* BlueWallet */ = {
isa = PBXGroup;
children = (
32C7944323B8879D00BE2AFA /* BlueWalletRelease.entitlements */,
32F0A2502310B0910095C559 /* BlueWallet.entitlements */,
008F07F21AC5B25A0029DE68 /* main.jsbundle */,
13B07FAF1A68108700A75B9A /* AppDelegate.h */,
@ -1266,7 +1268,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = BlueWallet/BlueWallet.entitlements;
CODE_SIGN_ENTITLEMENTS = BlueWallet/BlueWalletRelease.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.icloud-container-identifiers</key>
<array/>
<key>com.apple.developer.icloud-services</key>
<array>
<string>CloudDocuments</string>
</array>
<key>com.apple.developer.ubiquity-container-identifiers</key>
<array/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.io.bluewallet.bluewallet</string>
</array>
</dict>
</plist>

View file

@ -2,12 +2,29 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>BlueWallet</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeIconFiles</key>
<array/>
<key>CFBundleTypeName</key>
<string>PSBT</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSHandlerRank</key>
<string>Owner</string>
<key>LSItemContentTypes</key>
<array>
<string>io.bluewallet.psbt</string>
</array>
</dict>
</array>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
@ -58,18 +75,18 @@
</dict>
<key>NSAppleMusicUsageDescription</key>
<string>This alert should not show up as we do not require this data</string>
<key>NSFaceIDUsageDescription</key>
<string>In order to confirm your identity, we need your permission to use FaceID.</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>This alert should not show up as we do not require this data</string>
<key>NSCalendarsUsageDescription</key>
<string>This alert should not show up as we do not require this data</string>
<key>NSCameraUsageDescription</key>
<string>In order to quickly scan the recipient&apos;s address, we need your permission to use the camera to scan their QR Code.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This alert should not show up as we do not require this data</string>
<key>NSFaceIDUsageDescription</key>
<string>In order to confirm your identity, we need your permission to use FaceID.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>This alert should not show up as we do not require this data</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This alert should not show up as we do not require this data</string>
<key>NSMicrophoneUsageDescription</key>
<string>This alert should not show up as we do not require this data</string>
<key>NSMotionUsageDescription</key>
@ -116,7 +133,53 @@
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
</array>
<key>UTTypeDescription</key>
<string>PSBT</string>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>io.bluewallet.psbt</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>psbt</string>
</array>
</dict>
</dict>
</array>
<key>UTImportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
</array>
<key>UTTypeDescription</key>
<string>PSBT</string>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>io.bluewallet.psbt</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>psbt</string>
</array>
</dict>
</dict>
</array>
</dict>
</plist>

View file

@ -93,6 +93,8 @@
"react-native-camera": "3.4.0",
"react-native-default-preference": "1.4.1",
"react-native-device-info": "4.0.1",
"react-native-directory-picker": "git+https://github.com/BlueWallet/react-native-directory-picker.git#63307e646f72444ab83b619e579c55ee38cd162a",
"react-native-document-picker": "git+https://github.com/BlueWallet/react-native-document-picker.git#9ce83792db340d01b1361d24b19613658abef4aa",
"react-native-elements": "0.19.0",
"react-native-flexi-radio-button": "0.2.2",
"react-native-fs": "2.13.3",
@ -116,6 +118,7 @@
"react-native-snap-carousel": "3.8.4",
"react-native-sortable-list": "0.0.23",
"react-native-svg": "9.5.1",
"react-native-swiper": "git+https://github.com/BlueWallet/react-native-swiper.git#1.5.14",
"react-native-tcp": "git+https://github.com/aprock/react-native-tcp.git",
"react-native-tooltip": "git+https://github.com/marcosrdz/react-native-tooltip.git",
"react-native-vector-icons": "6.6.0",

View file

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

View file

@ -38,6 +38,8 @@ import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import { HDLegacyP2PKHWallet, HDSegwitBech32Wallet, HDSegwitP2SHWallet, LightningCustodianWallet, WatchOnlyWallet } from '../../class';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import { BitcoinTransaction } from '../../models/bitcoinTransactionInfo';
import DocumentPicker from 'react-native-document-picker';
import RNFS from 'react-native-fs';
const bitcoin = require('bitcoinjs-lib');
const bip21 = require('bip21');
let BigNumber = require('bignumber.js');
@ -58,9 +60,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 +184,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) {
@ -705,6 +709,36 @@ export default class SendDetails extends Component {
);
};
importTransaction = async () => {
try {
const res = await DocumentPicker.pick();
const file = await RNFS.readFile(res.uri, 'ascii');
const bufferDecoded = Buffer.from(file, 'ascii').toString('base64');
if (bufferDecoded) {
if (this.state.fromWallet.type === WatchOnlyWallet.type) {
// watch-only wallets with enabled HW wallet support have different flow. we have to show PSBT to user as QR code
// so he can scan it and sign it. then we have to scan it back from user (via camera and QR code), and ask
// user whether he wants to broadcast it
this.props.navigation.navigate('PsbtWithHardwareWallet', {
memo: this.state.memo,
fromWallet: this.state.fromWallet,
psbt: bufferDecoded,
isFirstPSBTAlreadyBase64: true,
});
this.setState({ isLoading: false });
return;
}
} else {
throw new Error();
}
} catch (err) {
if (!DocumentPicker.isCancel(err)) {
alert('The selected file does not contain a signed transaction that can be imported.');
}
}
this.setState({ isAdvancedTransactionOptionsVisible: false });
};
renderAdvancedTransactionOptionsModal = () => {
const isSendMaxUsed = this.state.addresses.some(element => element.amount === BitcoinUnit.MAX);
return (
@ -736,6 +770,9 @@ export default class SendDetails extends Component {
onSwitch={this.onReplaceableFeeSwitchValueChanged}
/>
)}
{this.state.fromWallet.use_with_hardware_wallet && (
<BlueListItem title="Import Transaction" hideChevron component={TouchableOpacity} onPress={this.importTransaction} />
)}
{this.state.fromWallet.allowBatchSend() && (
<>
<BlueListItem
@ -892,6 +929,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 +1105,7 @@ SendDetails.propTypes = {
getParam: PropTypes.func,
setParams: PropTypes.func,
state: PropTypes.shape({
routeName: PropTypes.string,
params: PropTypes.shape({
amount: PropTypes.number,
address: PropTypes.string,

View file

@ -1,6 +1,17 @@
/* global alert */
import React, { Component } from 'react';
import { ActivityIndicator, TouchableOpacity, View, Dimensions, Image, TextInput, Clipboard, Linking } from 'react-native';
import {
ActivityIndicator,
TouchableOpacity,
ScrollView,
View,
Dimensions,
Image,
TextInput,
Clipboard,
Linking,
Platform,
} from 'react-native';
import QRCode from 'react-native-qrcode-svg';
import { Icon, Text } from 'react-native-elements';
import {
@ -13,8 +24,12 @@ import {
BlueCopyToClipboardButton,
} from '../../BlueComponents';
import PropTypes from 'prop-types';
import Share from 'react-native-share';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import { RNCamera } from 'react-native-camera';
import RNFS from 'react-native-fs';
import DocumentPicker from 'react-native-document-picker';
import DirectoryPickerManager from 'react-native-directory-picker';
let loc = require('../../loc');
let EV = require('../../events');
let BlueElectrum = require('../../BlueElectrum');
@ -36,7 +51,10 @@ export default class PsbtWithHardwareWallet extends Component {
this.setState({ renderScanner: false }, () => {
console.log(ret.data);
try {
let Tx = this.state.fromWallet.combinePsbt(this.state.psbt.toBase64(), ret.data);
let Tx = this.state.fromWallet.combinePsbt(
this.state.isFirstPSBTAlreadyBase64 ? this.state.psbt : this.state.psbt.toBase64(),
this.state.isSecondPSBTAlreadyBase64 ? ret.data : ret.data.toBase64(),
);
this.setState({ txhex: Tx.toHex() });
} catch (Err) {
alert(Err);
@ -46,18 +64,19 @@ export default class PsbtWithHardwareWallet extends Component {
constructor(props) {
super(props);
this.state = {
isLoading: false,
renderScanner: false,
qrCodeHeight: height > width ? width - 40 : width / 2,
qrCodeHeight: height > width ? width - 40 : width / 3,
memo: props.navigation.getParam('memo'),
psbt: props.navigation.getParam('psbt'),
fromWallet: props.navigation.getParam('fromWallet'),
isFirstPSBTAlreadyBase64: props.navigation.getParam('isFirstPSBTAlreadyBase64'),
isSecondPSBTAlreadyBase64: false,
};
}
async componentDidMount() {
componentDidMount() {
console.log('send/psbtWithHardwareWallet - componentDidMount');
}
@ -185,6 +204,60 @@ export default class PsbtWithHardwareWallet extends Component {
);
}
exportPSBT = async () => {
const fileName = `${Date.now()}.psbt`;
if (Platform.OS === 'ios') {
const filePath = RNFS.TemporaryDirectoryPath + `/${fileName}`;
await RNFS.writeFile(filePath, this.state.isFirstPSBTAlreadyBase64 ? this.state.psbt : this.state.psbt.toBase64(), 'ascii');
Share.open({
url: 'file://' + filePath,
})
.catch(error => console.log(error))
.finally(() => {
RNFS.unlink(filePath);
});
} else if (Platform.OS === 'android') {
DirectoryPickerManager.showDirectoryPicker(null, async response => {
if (response.didCancel) {
console.log('User cancelled directory picker');
} else if (response.error) {
console.log('DirectoryPickerManager Error: ', response.error);
} else {
try {
await RNFS.writeFile(
response.path + `/${fileName}`,
this.state.isFirstPSBTAlreadyBase64 ? this.state.psbt : this.state.psbt.toBase64(),
'ascii',
);
alert('Successfully exported.');
RNFS.unlink(response.path + `/${fileName}`);
} catch (e) {
console.log(e);
alert(e);
}
}
});
}
};
openSignedTransaction = async () => {
try {
const res = await DocumentPicker.pick();
const file = await RNFS.readFile(res.uri, 'ascii');
const bufferDecoded = Buffer.from(file, 'ascii').toString('base64');
if (bufferDecoded) {
this.setState({ isSecondPSBTAlreadyBase64: true }, () => this.onBarCodeRead({ data: bufferDecoded }));
} else {
this.setState({ isSecondPSBTAlreadyBase64: false });
throw new Error();
}
} catch (err) {
if (!DocumentPicker.isCancel(err)) {
alert('The selected file does not contain a signed transaction that can be imported.');
}
}
};
render() {
if (this.state.isLoading) {
return (
@ -200,27 +273,58 @@ export default class PsbtWithHardwareWallet extends Component {
return (
<SafeBlueArea style={{ flex: 1 }}>
<View style={{ alignItems: 'center', justifyContent: 'space-between' }}>
<ScrollView centerContent contentContainerStyle={{ flexGrow: 1, justifyContent: 'space-between' }}>
<View style={{ flexDirection: 'row', justifyContent: 'center', paddingTop: 16, paddingBottom: 16 }}>
<BlueCard>
<BlueText>This is partially signed bitcoin transaction (PSBT). Please finish signing it with your hardware wallet.</BlueText>
<BlueSpacing20 />
<QRCode
value={this.state.psbt.toBase64()}
value={this.state.isFirstPSBTAlreadyBase64 ? this.state.psbt : this.state.psbt.toBase64()}
size={this.state.qrCodeHeight}
color={BlueApp.settings.foregroundColor}
logoBackgroundColor={BlueApp.settings.brandingColor}
ecl={'L'}
/>
<BlueSpacing20 />
<BlueButton onPress={() => this.setState({ renderScanner: true })} title={'Scan signed transaction'} />
<BlueButton
icon={{
name: 'qrcode',
type: 'font-awesome',
color: BlueApp.settings.buttonTextColor,
}}
onPress={() => this.setState({ renderScanner: true })}
title={'Scan Signed Transaction'}
/>
<BlueSpacing20 />
<BlueButton
icon={{
name: 'file-import',
type: 'material-community',
color: BlueApp.settings.buttonTextColor,
}}
onPress={this.openSignedTransaction}
title={'Open Signed Transaction'}
/>
<BlueSpacing20 />
<BlueButton
icon={{
name: 'share-alternative',
type: 'entypo',
color: BlueApp.settings.buttonTextColor,
}}
onPress={this.exportPSBT}
title={'Export'}
/>
<BlueSpacing20 />
<View style={{ justifyContent: 'center', alignItems: 'center' }}>
<BlueCopyToClipboardButton stringToCopy={this.state.psbt.toBase64()} displayText={'Copy to Clipboard'} />
<BlueCopyToClipboardButton
stringToCopy={this.state.isFirstPSBTAlreadyBase64 ? this.state.psbt : this.state.psbt.toBase64()}
displayText={'Copy to Clipboard'}
/>
</View>
</BlueCard>
</View>
</View>
</ScrollView>
</SafeBlueArea>
);
}

View file

@ -1,30 +1,69 @@
/* 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';
import DocumentPicker from 'react-native-document-picker';
import RNFS from 'react-native-fs';
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,
showFileImportButton = useNavigationParam('showFileImportButton') || false,
launchedBy = useNavigationParam('launchedBy'),
}) => {
const [isLoading, setIsLoading] = useState(false);
const { navigate } = useNavigation();
const onBarCodeRead = ret => {
if (!isLoading && !cameraPreviewIsPaused) {
setIsLoading(true);
try {
if (showCloseButton && launchedBy) {
navigate(launchedBy);
}
if (ret.additionalProperties) {
onBarScanned(ret.data, ret.additionalProperties);
} else {
onBarScanned(ret.data);
}
} catch (e) {
console.log(e);
}
}
setIsLoading(false);
};
cameraRef = null;
const showFilePicker = async () => {
setIsLoading(true);
try {
const res = await DocumentPicker.pick();
const file = await RNFS.readFile(res.uri);
const fileParsed = JSON.parse(file);
if (fileParsed.keystore.xpub) {
onBarCodeRead({ data: fileParsed.keystore.xpub, additionalProperties: { masterFingerprint: fileParsed.keystore.ckcc_xfp } });
} else {
throw new Error();
}
} catch (err) {
if (!DocumentPicker.isCancel(err)) {
alert('The selected file does not contain a wallet that can be imported.');
}
setIsLoading(false);
}
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 }}>
return (
<View style={{ flex: 1, backgroundColor: '#000000' }}>
{!cameraPreviewIsPaused && !isLoading && (
<RNCamera
captureAudio={false}
androidCameraPermissionOptions={{
@ -33,11 +72,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,23 +89,25 @@ 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,
height: 40,
backgroundColor: '#FFFFFF',
justifyContent: 'center',
borderRadius: 20,
position: 'absolute',
left: 24,
bottom: 48,
}}
onPress={() => {
if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.pausePreview();
)}
<TouchableOpacity
style={{
width: 40,
height: 40,
backgroundColor: '#FFFFFF',
justifyContent: 'center',
borderRadius: 20,
position: 'absolute',
left: 24,
bottom: 48,
}}
onPress={() => {
if (!isLoading) {
setIsLoading(true);
ImagePicker.launchImageLibrary(
{
title: null,
@ -77,30 +119,49 @@ 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>
{showFileImportButton && (
<TouchableOpacity
style={{
width: 40,
height: 40,
backgroundColor: '#FFFFFF',
justifyContent: 'center',
borderRadius: 20,
position: 'absolute',
left: 96,
bottom: 48,
}}
onPress={showFilePicker}
>
<Icon name="image" type="font-awesome" color="#0c2550" />
<Icon name="file-import" type="material-community" color="#0c2550" />
</TouchableOpacity>
</SafeBlueArea>
);
}
}
ScanQRCode.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.func,
dismiss: PropTypes.func,
getParam: PropTypes.func,
}),
)}
</View>
);
};
ScanQRCode.navigationOptions = {
header: null,
};
ScanQRCode.propTypes = {
launchedBy: PropTypes.string,
onBarScanned: PropTypes.func,
cameraPreviewIsPaused: PropTypes.bool,
showFileImportButton: PropTypes.bool,
showCloseButton: PropTypes.bool,
};
export default ScanQRCode;

View file

@ -20,6 +20,7 @@ import { HDSegwitP2SHWallet } from '../../class/hd-segwit-p2sh-wallet';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import Biometric from '../../class/biometrics';
import { HDSegwitBech32Wallet, WatchOnlyWallet } from '../../class';
import { ScrollView } from 'react-native-gesture-handler';
let EV = require('../../events');
let prompt = require('../../prompt');
/** @type {AppStorage} */
@ -55,6 +56,7 @@ export default class WalletDetails extends Component {
walletName: wallet.getLabel(),
wallet,
useWithHardwareWallet: !!wallet.use_with_hardware_wallet,
masterFingerprint: wallet.masterFingerprint ? String(wallet.masterFingerprint) : '',
};
this.props.navigation.setParams({ isLoading, saveAction: () => this.setLabel() });
}
@ -71,6 +73,7 @@ export default class WalletDetails extends Component {
this.props.navigation.setParams({ isLoading: true });
this.setState({ isLoading: true }, async () => {
this.state.wallet.setLabel(this.state.walletName);
this.state.wallet.masterFingerprint = Number(this.state.masterFingerprint);
BlueApp.saveToDisk();
alert('Wallet updated.');
this.props.navigation.goBack(null);
@ -122,7 +125,7 @@ export default class WalletDetails extends Component {
return (
<SafeBlueArea style={{ flex: 1 }}>
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
<View style={{ flex: 1 }}>
<ScrollView contentContainerStyle={{ flexGrow: 1 }}>
<BlueCard style={{ alignItems: 'center', flex: 1 }}>
{(() => {
if (this.state.wallet.getAddress()) {
@ -158,18 +161,20 @@ export default class WalletDetails extends Component {
placeholder={loc.send.details.note_placeholder}
value={this.state.walletName}
onChangeText={text => {
if (text.trim().length === 0) {
text = this.state.wallet.getLabel();
}
this.setState({ walletName: text });
}}
onBlur={() => {
if (this.state.walletName.trim().length === 0) {
this.setState({ walletName: this.state.wallet.getLabel() });
}
}}
numberOfLines={1}
style={{ flex: 1, marginHorizontal: 8, minHeight: 33 }}
editable={!this.state.isLoading}
underlineColorAndroid="transparent"
/>
</View>
<BlueSpacing20 />
<Text style={{ color: '#0c2550', fontWeight: '500', fontSize: 14, marginVertical: 12 }}>
{loc.wallets.details.type.toLowerCase()}
</Text>
@ -182,15 +187,48 @@ export default class WalletDetails extends Component {
)}
<View>
<BlueSpacing20 />
{this.state.wallet.type === WatchOnlyWallet.type && this.state.wallet.getSecret().startsWith('zpub') && (
<React.Fragment>
<>
<Text style={{ color: '#0c2550', fontWeight: '500', fontSize: 14, marginVertical: 16 }}>{'advanced'}</Text>
<BlueText>Master Fingerprint</BlueText>
<BlueSpacing20 />
<View
style={{
flexDirection: 'row',
borderColor: '#d2d2d2',
borderBottomColor: '#d2d2d2',
borderWidth: 1.0,
borderBottomWidth: 0.5,
backgroundColor: '#f5f5f5',
minHeight: 44,
height: 44,
alignItems: 'center',
borderRadius: 4,
}}
>
<TextInput
placeholder="Master Fingerprint"
value={this.state.masterFingerprint}
onChangeText={text => {
if (isNaN(text)) {
return;
}
this.setState({ masterFingerprint: text });
}}
numberOfLines={1}
style={{ flex: 1, marginHorizontal: 8, minHeight: 33 }}
editable={!this.state.isLoading}
keyboardType="decimal-pad"
underlineColorAndroid="transparent"
/>
</View>
<BlueSpacing20 />
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<BlueText>{'Use with hardware wallet'}</BlueText>
<Switch value={this.state.useWithHardwareWallet} onValueChange={value => this.onUseWithHardwareWalletSwitch(value)} />
</View>
<BlueSpacing20 />
</React.Fragment>
</>
)}
<BlueButton
@ -285,7 +323,7 @@ export default class WalletDetails extends Component {
</TouchableOpacity>
</View>
</BlueCard>
</View>
</ScrollView>
</TouchableWithoutFeedback>
</SafeBlueArea>
);

View file

@ -35,9 +35,9 @@ const WalletsImport = () => {
importMnemonic(importText);
};
const importMnemonic = importText => {
const importMnemonic = (importText, additionalProperties) => {
try {
WalletImport.processImportText(importText);
WalletImport.processImportText(importText, additionalProperties);
dismiss();
} catch (error) {
alert(loc.wallets.import.error);
@ -45,9 +45,9 @@ const WalletsImport = () => {
}
};
const onBarScanned = value => {
const onBarScanned = (value, additionalProperties) => {
setImportText(value);
importMnemonic(value);
importMnemonic(value, additionalProperties);
};
return (
@ -110,7 +110,7 @@ const WalletsImport = () => {
<BlueButtonLink
title={loc.wallets.import.scan_qr}
onPress={() => {
navigate('ScanQrAddress', { onBarScanned });
navigate('ScanQrAddress', { launchedBy: 'ImportWallet', onBarScanned, showFileImportButton: true });
}}
/>
</View>

View file

@ -1,6 +1,17 @@
/* global alert */
import React, { Component } from 'react';
import { View, TouchableOpacity, Text, FlatList, InteractionManager, RefreshControl, ScrollView, Alert } from 'react-native';
import {
View,
StatusBar,
TouchableOpacity,
Text,
StyleSheet,
FlatList,
InteractionManager,
RefreshControl,
ScrollView,
Alert,
} from 'react-native';
import { BlueLoading, SafeBlueArea, WalletsCarousel, BlueList, BlueHeaderDefaultMain, BlueTransactionListItem } from '../../BlueComponents';
import { Icon } from 'react-native-elements';
import { NavigationEvents } from 'react-navigation';
@ -8,6 +19,9 @@ import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import PropTypes from 'prop-types';
import { PlaceholderWallet } from '../../class';
import WalletImport from '../../class/walletImport';
import Swiper from 'react-native-swiper';
import ScanQRCode from '../send/scanQrAddress';
import DeeplinkSchemaMatch from '../../class/deeplinkSchemaMatch';
let EV = require('../../events');
let A = require('../../analytics');
/** @type {AppStorage} */
@ -16,23 +30,8 @@ let loc = require('../../loc');
let BlueElectrum = require('../../BlueElectrum');
export default class WalletsList extends Component {
static navigationOptions = ({ navigation }) => ({
headerStyle: {
backgroundColor: '#FFFFFF',
borderBottomWidth: 0,
elevation: 0,
},
headerRight: (
<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);
@ -299,21 +298,47 @@ export default class WalletsList extends Component {
}
};
onSwiperIndexChanged = index => {
StatusBar.setBarStyle(index === 1 ? 'dark-content' : 'light-content');
this.setState({ cameraPreviewIsPaused: index === 1 });
};
onBarScanned = value => {
DeeplinkSchemaMatch.navigationRouteFor({ url: value }, completionValue => {
ReactNativeHapticFeedback.trigger('impactLight', { ignoreAndroidSystemSettings: false });
this.props.navigation.navigate(completionValue);
});
};
_renderItem = data => {
return <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 });
}}
onWillBlur={() => this.setState({ cameraPreviewIsPaused: true })}
/>
<ScrollView
contentContainerStyle={{ flex: 1 }}
refreshControl={
<RefreshControl
onRefresh={() => this.refreshTransactions()}
@ -322,65 +347,112 @@ export default class WalletsList extends Component {
/>
}
>
<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}
handleClick={index => {
this.handleClick(index);
}}
handleLongPress={this.handleLongPress}
onSnapToItem={index => {
this.onSnapToItem(index);
}}
ref={c => (this.walletsCarousel = c)}
/>
<BlueList>
<FlatList
ListHeaderComponent={this.renderListHeaderComponent}
ListEmptyComponent={
<View style={{ top: 50, height: 100 }}>
<Text
style={{
fontSize: 18,
color: '#9aa0aa',
textAlign: 'center',
<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={
!BlueApp.getWallets().some(wallet => wallet.type === PlaceholderWallet.type)
? () => this.props.navigation.navigate('AddWallet')
: null
}
/>
<WalletsCarousel
removeClippedSubviews={false}
data={this.state.wallets}
handleClick={index => {
this.handleClick(index);
}}
>
{loc.wallets.list.empty_txs1}
</Text>
<Text
style={{
fontSize: 18,
color: '#9aa0aa',
textAlign: 'center',
handleLongPress={this.handleLongPress}
onSnapToItem={index => {
this.onSnapToItem(index);
}}
>
{loc.wallets.list.empty_txs2}
</Text>
</View>
}
data={this.state.dataSource}
extraData={this.state.dataSource}
keyExtractor={this._keyExtractor}
renderItem={this._renderItem}
/>
</BlueList>
ref={c => (this.walletsCarousel = c)}
/>
<BlueList>
<FlatList
ListHeaderComponent={this.renderListHeaderComponent}
ListEmptyComponent={
<View style={{ top: 50, height: 100 }}>
<Text
style={{
fontSize: 18,
color: '#9aa0aa',
textAlign: 'center',
}}
>
{loc.wallets.list.empty_txs1}
</Text>
<Text
style={{
fontSize: 18,
color: '#9aa0aa',
textAlign: 'center',
}}
>
{loc.wallets.list.empty_txs2}
</Text>
</View>
}
data={this.state.dataSource}
extraData={this.state.dataSource}
keyExtractor={this._keyExtractor}
renderItem={this._renderItem}
/>
</BlueList>
</ScrollView>
</View>
</SafeBlueArea>
</Swiper>
</ScrollView>
</SafeBlueArea>
</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,
}),
};

View file

@ -16,6 +16,7 @@ import {
StatusBar,
Linking,
KeyboardAvoidingView,
Alert,
} from 'react-native';
import PropTypes from 'prop-types';
import { NavigationEvents } from 'react-navigation';
@ -29,7 +30,7 @@ import {
} from '../../BlueComponents';
import WalletGradient from '../../class/walletGradient';
import { Icon } from 'react-native-elements';
import { LightningCustodianWallet } from '../../class';
import { LightningCustodianWallet, HDSegwitBech32Wallet } from '../../class';
import Handoff from 'react-native-handoff';
import Modal from 'react-native-modal';
import NavigationService from '../../NavigationService';
@ -400,7 +401,7 @@ export default class WalletTransactions extends Component {
}
};
async onWillBlur() {
onWillBlur() {
StatusBar.setBarStyle('dark-content');
}
@ -409,6 +410,14 @@ export default class WalletTransactions extends Component {
clearInterval(this.interval);
}
navigateToSendScreen = () => {
this.props.navigation.navigate('SendDetails', {
fromAddress: this.state.wallet.getAddress(),
fromSecret: this.state.wallet.getSecret(),
fromWallet: this.state.wallet,
});
};
renderItem = item => {
return (
<BlueTransactionListItem
@ -569,18 +578,45 @@ export default class WalletTransactions extends Component {
})()}
{(() => {
if (this.state.wallet.allowSend()) {
if (
this.state.wallet.allowSend() ||
(this.state.wallet._hdWalletInstance instanceof HDSegwitBech32Wallet && this.state.wallet._hdWalletInstance.allowSend())
) {
return (
<BlueSendButtonIcon
onPress={() => {
if (this.state.wallet.chain === Chain.OFFCHAIN) {
navigate('ScanLndInvoice', { fromSecret: this.state.wallet.getSecret() });
} else {
navigate('SendDetails', {
fromAddress: this.state.wallet.getAddress(),
fromSecret: this.state.wallet.getSecret(),
fromWallet: this.state.wallet,
});
if (
this.state.wallet._hdWalletInstance instanceof HDSegwitBech32Wallet &&
this.state.wallet._hdWalletInstance.allowSend()
) {
if (this.state.wallet.use_with_hardware_wallet) {
this.navigateToSendScreen();
} else {
Alert.alert(
'Wallet',
'This wallet is not being used in conjunction with a hardwarde wallet. Would you like to enable hardware wallet use?',
[
{
text: loc._.ok,
onPress: async () => {
this.state.wallet.use_with_hardware_wallet = true;
await BlueApp.saveToDisk();
this.navigateToSendScreen();
},
style: 'default',
},
{ text: loc.send.details.cancel, onPress: () => {}, style: 'cancel' },
],
{ cancelable: false },
);
}
} else {
this.navigateToSendScreen();
}
}
}}
/>

View file

@ -0,0 +1,50 @@
/* global describe, it, expect */
import DeeplinkSchemaMatch from '../../class/deeplinkSchemaMatch';
const assert = require('assert');
describe('unit - DeepLinkSchemaMatch', function() {
it('hasSchema', () => {
const hasSchema = DeeplinkSchemaMatch.hasSchema('bitcoin:12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG');
assert.ok(hasSchema);
});
it('isBitcoin Address', () => {
assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG'));
});
it('isLighting Invoice', () => {
assert.ok(
DeeplinkSchemaMatch.isLightningInvoice(
'lightning:lnbc10u1pwjqwkkpp5vlc3tttdzhpk9fwzkkue0sf2pumtza7qyw9vucxyyeh0yaqq66yqdq5f38z6mmwd3ujqar9wd6qcqzpgxq97zvuqrzjqvgptfurj3528snx6e3dtwepafxw5fpzdymw9pj20jj09sunnqmwqz9hx5qqtmgqqqqqqqlgqqqqqqgqjq5duu3fs9xq9vn89qk3ezwpygecu4p3n69wm3tnl28rpgn2gmk5hjaznemw0gy32wrslpn3g24khcgnpua9q04fttm2y8pnhmhhc2gncplz0zde',
),
);
});
it('isBoth Bitcoin & Invoice', () => {
assert.ok(
DeeplinkSchemaMatch.isBothBitcoinAndLightning(
'bitcoin:1DamianM2k8WfNEeJmyqSe2YW1upB7UATx?amount=0.000001&lightning=lnbc1u1pwry044pp53xlmkghmzjzm3cljl6729cwwqz5hhnhevwfajpkln850n7clft4sdqlgfy4qv33ypmj7sj0f32rzvfqw3jhxaqcqzysxq97zvuq5zy8ge6q70prnvgwtade0g2k5h2r76ws7j2926xdjj2pjaq6q3r4awsxtm6k5prqcul73p3atveljkn6wxdkrcy69t6k5edhtc6q7lgpe4m5k4',
),
);
});
it('isLnurl', () => {
assert.ok(
DeeplinkSchemaMatch.isLnUrl(
'LNURL1DP68GURN8GHJ7UM9WFMXJCM99E3K7MF0V9CXJ0M385EKVCENXC6R2C35XVUKXEFCV5MKVV34X5EKZD3EV56NYD3HXQURZEPEXEJXXEPNXSCRVWFNV9NXZCN9XQ6XYEFHVGCXXCMYXYMNSERXFQ5FNS',
),
);
});
it('navigationForRoute', () => {
const event = { uri: '12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG' };
DeeplinkSchemaMatch.navigationRouteFor(event, navValue => {
assert.strictEqual(navValue, {
routeName: 'SendDetails',
params: {
uri: event.url,
},
});
});
});
});