Merge branch 'master' of https://github.com/BlueWallet/BlueWallet into settingsui

This commit is contained in:
marcosrodriguezseminole 2020-03-02 09:36:16 -05:00
commit 4a5a55c254
66 changed files with 1728 additions and 635 deletions

View file

@ -1382,7 +1382,7 @@ export class NewWalletPanel extends Component {
style={{ style={{
padding: 15, padding: 15,
borderRadius: 10, borderRadius: 10,
minHeight: 164, minHeight: Platform.OS === 'ios' ? 164 : 181,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
}} }}
@ -1838,7 +1838,9 @@ export class WalletsCarousel extends Component {
<NewWalletPanel <NewWalletPanel
onPress={() => { onPress={() => {
if (WalletsCarousel.handleClick) { if (WalletsCarousel.handleClick) {
this.onPressedOut();
WalletsCarousel.handleClick(index); WalletsCarousel.handleClick(index);
this.onPressedOut();
} }
}} }}
/> />
@ -1858,7 +1860,9 @@ export class WalletsCarousel extends Component {
onPressOut={item.getIsFailure() ? this.onPressedOut : null} onPressOut={item.getIsFailure() ? this.onPressedOut : null}
onPress={() => { onPress={() => {
if (item.getIsFailure() && WalletsCarousel.handleClick) { if (item.getIsFailure() && WalletsCarousel.handleClick) {
this.onPressedOut();
WalletsCarousel.handleClick(index); WalletsCarousel.handleClick(index);
this.onPressedOut();
} }
}} }}
> >
@ -1926,7 +1930,9 @@ export class WalletsCarousel extends Component {
onLongPress={WalletsCarousel.handleLongPress} onLongPress={WalletsCarousel.handleLongPress}
onPress={() => { onPress={() => {
if (WalletsCarousel.handleClick) { if (WalletsCarousel.handleClick) {
this.onPressedOut();
WalletsCarousel.handleClick(index); WalletsCarousel.handleClick(index);
this.onPressedOut();
} }
}} }}
> >
@ -2077,7 +2083,7 @@ export class BlueAddressInput extends Component {
value={this.props.address} value={this.props.address}
style={{ flex: 1, marginHorizontal: 8, minHeight: 33 }} style={{ flex: 1, marginHorizontal: 8, minHeight: 33 }}
editable={!this.props.isLoading} editable={!this.props.isLoading}
onSubmitEditing={() => Keyboard.dismiss()} onSubmitEditing={Keyboard.dismiss}
{...this.props} {...this.props}
/> />
<TouchableOpacity <TouchableOpacity

View file

@ -194,6 +194,12 @@ const LNDCreateInvoiceStackNavigator = createStackNavigator({
LNDCreateInvoice: { LNDCreateInvoice: {
screen: LNDCreateInvoice, screen: LNDCreateInvoice,
}, },
SelectWallet: {
screen: SelectWallet,
navigationOptions: {
headerLeft: null,
},
},
LNDViewInvoice: { LNDViewInvoice: {
screen: LNDViewInvoice, screen: LNDViewInvoice,
swipeEnabled: false, swipeEnabled: false,
@ -210,6 +216,7 @@ const CreateWalletStackNavigator = createStackNavigator({
}, },
ImportWallet: { ImportWallet: {
screen: ImportWallet, screen: ImportWallet,
routeName: 'ImportWallet',
}, },
PleaseBackup: { PleaseBackup: {
screen: PleaseBackup, screen: PleaseBackup,

View file

@ -119,7 +119,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1 versionCode 1
versionName "4.9.1" versionName "5.0.0"
multiDexEnabled true multiDexEnabled true
missingDimensionStrategy 'react-native-camera', 'general' missingDimensionStrategy 'react-native-camera', 'general'
} }

View file

@ -4,7 +4,7 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application <application
android:name=".MainApplication" android:name=".MainApplication"
android:label="@string/app_name" android:label="@string/app_name"
@ -34,6 +34,26 @@
<data android:scheme="lapp" /> <data android:scheme="lapp" />
<data android:scheme="blue" /> <data android:scheme="blue" />
</intent-filter> </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>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" /> <activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
</application> </application>

View file

@ -8,6 +8,7 @@ const BlueElectrum = require('../BlueElectrum');
const HDNode = require('bip32'); const HDNode = require('bip32');
const coinSelectAccumulative = require('coinselect/accumulative'); const coinSelectAccumulative = require('coinselect/accumulative');
const coinSelectSplit = require('coinselect/split'); const coinSelectSplit = require('coinselect/split');
const reverse = require('buffer-reverse');
const { RNRandomBytes } = NativeModules; const { RNRandomBytes } = NativeModules;
@ -635,6 +636,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
async fetchUtxo() { async fetchUtxo() {
// considering only confirmed balance // considering only confirmed balance
// also, fetching utxo of addresses that only have some balance
let addressess = []; let addressess = [];
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
@ -717,9 +719,10 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
* @param changeAddress {String} Excessive coins will go back to that address * @param changeAddress {String} Excessive coins will go back to that address
* @param sequence {Number} Used in RBF * @param sequence {Number} Used in RBF
* @param skipSigning {boolean} Whether we should skip signing, use returned `psbt` in that case * @param skipSigning {boolean} Whether we should skip signing, use returned `psbt` in that case
* @param masterFingerprint {number} Decimal number of wallet's master fingerprint
* @returns {{outputs: Array, tx: Transaction, inputs: Array, fee: Number, psbt: Psbt}} * @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'); if (!changeAddress) throw new Error('No change address provided');
sequence = sequence || AbstractHDElectrumWallet.defaultRBFSequence; sequence = sequence || AbstractHDElectrumWallet.defaultRBFSequence;
@ -756,7 +759,15 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
if (!input.address || !this._getWifForAddress(input.address)) throw new Error('Internal error: no address or WIF to sign input'); 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 pubkey = this._getPubkeyByAddress(input.address);
let masterFingerprint = Buffer.from([0x00, 0x00, 0x00, 0x00]); let masterFingerprintBuffer;
if (masterFingerprint) {
let masterFingerprintHex = Number(masterFingerprint).toString(16);
if (masterFingerprintHex.length < 8) masterFingerprintHex = '0' + masterFingerprintHex; // conversion without explicit zero might result in lost byte
const hexBuffer = Buffer.from(masterFingerprintHex, '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 // 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 // should be from root. basically, fingerprint should be provided from outside by user when importing zpub
let path = this._getDerivationPathByAddress(input.address); let path = this._getDerivationPathByAddress(input.address);
@ -767,7 +778,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
sequence, sequence,
bip32Derivation: [ bip32Derivation: [
{ {
masterFingerprint, masterFingerprint: masterFingerprintBuffer,
path, path,
pubkey, pubkey,
}, },
@ -789,7 +800,17 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
let path = this._getDerivationPathByAddress(output.address); let path = this._getDerivationPathByAddress(output.address);
let pubkey = this._getPubkeyByAddress(output.address); let pubkey = this._getPubkeyByAddress(output.address);
let masterFingerprint = Buffer.from([0x00, 0x00, 0x00, 0x00]); let masterFingerprintBuffer;
if (masterFingerprint) {
let masterFingerprintHex = Number(masterFingerprint).toString(16);
if (masterFingerprintHex.length < 8) masterFingerprintHex = '0' + masterFingerprintHex; // conversion without explicit zero might result in lost byte
const hexBuffer = Buffer.from(masterFingerprintHex, '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 // 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 // should be from root. basically, fingerprint should be provided from outside by user when importing zpub
@ -801,7 +822,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
if (change) { if (change) {
outputData['bip32Derivation'] = [ outputData['bip32Derivation'] = [
{ {
masterFingerprint, masterFingerprint: masterFingerprintBuffer,
path, path,
pubkey, pubkey,
}, },

View file

@ -1,5 +1,6 @@
import { BitcoinUnit, Chain } from '../models/bitcoinUnits'; import { BitcoinUnit, Chain } from '../models/bitcoinUnits';
const createHash = require('create-hash'); const createHash = require('create-hash');
export class AbstractWallet { export class AbstractWallet {
static type = 'abstract'; static type = 'abstract';
static typeReadable = 'abstract'; static typeReadable = 'abstract';
@ -128,6 +129,19 @@ export class AbstractWallet {
setSecret(newSecret) { setSecret(newSecret) {
this.secret = newSecret.trim(); this.secret = newSecret.trim();
try {
const parsedSecret = JSON.parse(this.secret);
if (parsedSecret && parsedSecret.keystore && parsedSecret.keystore.xpub) {
let masterFingerprint = false;
if (parsedSecret.keystore.ckcc_xfp) {
// It is a ColdCard Hardware Wallet
masterFingerprint = Number(parsedSecret.keystore.ckcc_xfp);
}
this.secret = parsedSecret.keystore.xpub;
this.masterFingerprint = masterFingerprint;
}
} catch (_) {}
return this; return this;
} }
@ -144,4 +158,8 @@ export class AbstractWallet {
getAddressAsync() { getAddressAsync() {
return new Promise(resolve => resolve(this.getAddress())); return new Promise(resolve => resolve(this.getAddress()));
} }
useWithHardwareWalletEnabled() {
return false;
}
} }

View file

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

View file

@ -1,8 +1,12 @@
import { AppStorage, LightningCustodianWallet } from './'; import { AppStorage, LightningCustodianWallet } from './';
import AsyncStorage from '@react-native-community/async-storage'; import AsyncStorage from '@react-native-community/async-storage';
import BitcoinBIP70TransactionDecode from '../bip70/bip70'; import BitcoinBIP70TransactionDecode from '../bip70/bip70';
import RNFS from 'react-native-fs';
import url from 'url';
import { Chain } from '../models/bitcoinUnits';
const bitcoin = require('bitcoinjs-lib'); const bitcoin = require('bitcoinjs-lib');
const BlueApp = require('../BlueApp'); const BlueApp: AppStorage = require('../BlueApp');
class DeeplinkSchemaMatch { class DeeplinkSchemaMatch {
static hasSchema(schemaString) { static hasSchema(schemaString) {
if (typeof schemaString !== 'string' || schemaString.length <= 0) return false; if (typeof schemaString !== 'string' || schemaString.length <= 0) return false;
@ -30,6 +34,21 @@ class DeeplinkSchemaMatch {
if (typeof event.url !== 'string') { if (typeof event.url !== 'string') {
return; return;
} }
if (DeeplinkSchemaMatch.isPossiblyPSBTFile(event.url)) {
RNFS.readFile(event.url)
.then(file => {
if (file) {
completionHandler({
routeName: 'PsbtWithHardwareWallet',
params: {
deepLinkPSBT: file,
},
});
}
})
.catch(e => console.warn(e));
return;
}
let isBothBitcoinAndLightning; let isBothBitcoinAndLightning;
try { try {
isBothBitcoinAndLightning = DeeplinkSchemaMatch.isBothBitcoinAndLightning(event.url); isBothBitcoinAndLightning = DeeplinkSchemaMatch.isBothBitcoinAndLightning(event.url);
@ -40,7 +59,8 @@ class DeeplinkSchemaMatch {
completionHandler({ completionHandler({
routeName: 'HandleOffchainAndOnChain', routeName: 'HandleOffchainAndOnChain',
params: { params: {
onWalletSelect: this.isBothBitcoinAndLightningWalletSelect, onWalletSelect: wallet =>
completionHandler(DeeplinkSchemaMatch.isBothBitcoinAndLightningOnWalletSelect(wallet, isBothBitcoinAndLightning)),
}, },
}); });
} else if (DeeplinkSchemaMatch.isBitcoinAddress(event.url) || BitcoinBIP70TransactionDecode.matchesPaymentURL(event.url)) { } else if (DeeplinkSchemaMatch.isBitcoinAddress(event.url) || BitcoinBIP70TransactionDecode.matchesPaymentURL(event.url)) {
@ -95,7 +115,7 @@ class DeeplinkSchemaMatch {
if (!haveLnWallet) { if (!haveLnWallet) {
// need to create one // need to create one
let w = new LightningCustodianWallet(); let w = new LightningCustodianWallet();
w.setLabel(this.state.label || w.typeReadable); w.setLabel(w.typeReadable);
try { try {
let lndhub = await AsyncStorage.getItem(AppStorage.LNDHUB); let lndhub = await AsyncStorage.getItem(AppStorage.LNDHUB);
@ -128,17 +148,14 @@ class DeeplinkSchemaMatch {
return; return;
} }
this.navigator && completionHandler({
this.navigator.dispatch( routeName: 'LappBrowser',
completionHandler({ params: {
routeName: 'LappBrowser', fromSecret: lnWallet.getSecret(),
params: { fromWallet: lnWallet,
fromSecret: lnWallet.getSecret(), url: urlObject.query.url,
fromWallet: lnWallet, },
url: urlObject.query.url, });
},
}),
);
break; break;
} }
} }
@ -146,6 +163,34 @@ class DeeplinkSchemaMatch {
} }
} }
static isTXNFile(filePath) {
return filePath.toLowerCase().startsWith('file:') && filePath.toLowerCase().endsWith('.txn');
}
static isPossiblyPSBTFile(filePath) {
return filePath.toLowerCase().startsWith('file:') && filePath.toLowerCase().endsWith('-signed.psbt');
}
static isBothBitcoinAndLightningOnWalletSelect(wallet, uri) {
if (wallet.chain === Chain.ONCHAIN) {
return {
routeName: 'SendDetails',
params: {
uri: uri.bitcoin,
fromWallet: wallet,
},
};
} else if (wallet.chain === Chain.OFFCHAIN) {
return {
routeName: 'ScanLndInvoice',
params: {
uri: uri.lndInvoice,
fromSecret: wallet.getSecret(),
},
};
}
}
static isBitcoinAddress(address) { static isBitcoinAddress(address) {
address = address address = address
.replace('bitcoin:', '') .replace('bitcoin:', '')

View file

@ -1,5 +1,6 @@
import { LegacyWallet } from './legacy-wallet'; import { LegacyWallet } from './legacy-wallet';
import Frisbee from 'frisbee'; import Frisbee from 'frisbee';
import bolt11 from 'bolt11';
import { BitcoinUnit, Chain } from '../models/bitcoinUnits'; import { BitcoinUnit, Chain } from '../models/bitcoinUnits';
export class LightningCustodianWallet extends LegacyWallet { export class LightningCustodianWallet extends LegacyWallet {
@ -515,7 +516,7 @@ export class LightningCustodianWallet extends LegacyWallet {
* Example return: * Example return:
* { destination: '03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f', * { destination: '03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f',
* payment_hash: 'faf996300a468b668c58ca0702a12096475a0dd2c3dde8e812f954463966bcf4', * payment_hash: 'faf996300a468b668c58ca0702a12096475a0dd2c3dde8e812f954463966bcf4',
* num_satoshisnum_satoshis: '100', * num_satoshis: '100',
* timestamp: '1535116657', * timestamp: '1535116657',
* expiry: '3600', * expiry: '3600',
* description: 'hundredSatoshis blitzhub', * description: 'hundredSatoshis blitzhub',
@ -527,31 +528,46 @@ export class LightningCustodianWallet extends LegacyWallet {
* @param invoice BOLT invoice string * @param invoice BOLT invoice string
* @return {Promise.<Object>} * @return {Promise.<Object>}
*/ */
async decodeInvoice(invoice) { decodeInvoice(invoice) {
await this.checkLogin(); let { payeeNodeKey, tags, satoshis, millisatoshis, timestamp } = bolt11.decode(invoice);
let response = await this._api.get('/decodeinvoice?invoice=' + invoice, { let decoded = {
headers: { destination: payeeNodeKey,
'Access-Control-Allow-Origin': '*', num_satoshis: satoshis ? satoshis.toString() : '0',
'Content-Type': 'application/json', num_millisatoshis: millisatoshis ? millisatoshis.toString() : '0',
Authorization: 'Bearer' + ' ' + this.access_token, timestamp: timestamp.toString(),
}, fallback_addr: '',
}); route_hints: [],
};
let json = response.body; for (let i = 0; i < tags.length; i++) {
if (typeof json === 'undefined') { let { tagName, data } = tags[i];
throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.body)); switch (tagName) {
case 'payment_hash':
decoded.payment_hash = data;
break;
case 'purpose_commit_hash':
decoded.description_hash = data;
break;
case 'min_final_cltv_expiry':
decoded.cltv_expiry = data.toString();
break;
case 'expire_time':
decoded.expiry = data.toString();
break;
case 'description':
decoded.description = data;
break;
}
} }
if (json && json.error) { if (!decoded.expiry) decoded.expiry = '3600'; // default
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
if (parseInt(decoded.num_satoshis) === 0 && decoded.num_millisatoshis > 0) {
decoded.num_satoshis = (decoded.num_millisatoshis / 1000).toString();
} }
if (!json.payment_hash) { return (this.decoded_invoice_raw = decoded);
throw new Error('API unexpected response: ' + JSON.stringify(response.body));
}
return (this.decoded_invoice_raw = json);
} }
async fetchInfo() { async fetchInfo() {
@ -602,6 +618,49 @@ export class LightningCustodianWallet extends LegacyWallet {
allowReceive() { allowReceive() {
return true; return true;
} }
/**
* Example return:
* { destination: '03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f',
* payment_hash: 'faf996300a468b668c58ca0702a12096475a0dd2c3dde8e812f954463966bcf4',
* num_satoshis: '100',
* timestamp: '1535116657',
* expiry: '3600',
* description: 'hundredSatoshis blitzhub',
* description_hash: '',
* fallback_addr: '',
* cltv_expiry: '10',
* route_hints: [] }
*
* @param invoice BOLT invoice string
* @return {Promise.<Object>}
*/
async decodeInvoiceRemote(invoice) {
await this.checkLogin();
let response = await this._api.get('/decodeinvoice?invoice=' + invoice, {
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
Authorization: 'Bearer' + ' ' + this.access_token,
},
});
let json = response.body;
if (typeof json === 'undefined') {
throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.body));
}
if (json && json.error) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
if (!json.payment_hash) {
throw new Error('API unexpected response: ' + JSON.stringify(response.body));
}
return (this.decoded_invoice_raw = json);
}
} }
/* /*

View file

@ -18,24 +18,41 @@ const BlueApp = require('../BlueApp');
const loc = require('../loc'); const loc = require('../loc');
export default class WalletImport { export default class WalletImport {
static async _saveWallet(w) { /**
*
* @param w
* @param additionalProperties key-values passed from outside. Used only to set up `masterFingerprint` property for watch-only wallet
* @returns {Promise<void>}
* @private
*/
static async _saveWallet(w, additionalProperties) {
try { try {
const wallet = BlueApp.getWallets().some(wallet => wallet.getSecret() === w.secret && wallet.type !== PlaceholderWallet.type); const wallet = BlueApp.getWallets().some(wallet => wallet.getSecret() === w.secret && wallet.type !== PlaceholderWallet.type);
if (wallet) { if (wallet) {
alert('This wallet has been previously imported.'); alert('This wallet has been previously imported.');
WalletImport.removePlaceholderWallet(); WalletImport.removePlaceholderWallet();
} else { } else {
alert(loc.wallets.import.success);
ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false }); ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false });
w.setLabel(loc.wallets.import.imported + ' ' + w.typeReadable); w.setLabel(loc.wallets.import.imported + ' ' + w.typeReadable);
w.setUserHasSavedExport(true); w.setUserHasSavedExport(true);
if (additionalProperties) {
for (const [key, value] of Object.entries(additionalProperties)) {
w[key] = value;
}
}
WalletImport.removePlaceholderWallet(); WalletImport.removePlaceholderWallet();
BlueApp.wallets.push(w); BlueApp.wallets.push(w);
await BlueApp.saveToDisk(); await BlueApp.saveToDisk();
A(A.ENUM.CREATED_WALLET); A(A.ENUM.CREATED_WALLET);
alert(loc.wallets.import.success);
} }
EV(EV.enum.WALLETS_COUNT_CHANGED); 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() { static removePlaceholderWallet() {
@ -58,7 +75,13 @@ export default class WalletImport {
return BlueApp.getWallets().some(wallet => wallet.type === PlaceholderWallet.type); return BlueApp.getWallets().some(wallet => wallet.type === PlaceholderWallet.type);
} }
static async processImportText(importText) { /**
*
* @param importText
* @param additionalProperties key-values passed from outside. Used only to set up `masterFingerprint` property for watch-only wallet
* @returns {Promise<void>}
*/
static async processImportText(importText, additionalProperties) {
if (WalletImport.isCurrentlyImportingWallet()) { if (WalletImport.isCurrentlyImportingWallet()) {
return; return;
} }
@ -209,7 +232,7 @@ export default class WalletImport {
if (watchOnly.valid()) { if (watchOnly.valid()) {
await watchOnly.fetchTransactions(); await watchOnly.fetchTransactions();
await watchOnly.fetchBalance(); await watchOnly.fetchBalance();
return WalletImport._saveWallet(watchOnly); return WalletImport._saveWallet(watchOnly, additionalProperties);
} }
// nope? // nope?

View file

@ -11,21 +11,26 @@ export class WatchOnlyWallet extends LegacyWallet {
constructor() { constructor() {
super(); super();
this.use_with_hardware_wallet = false; this.use_with_hardware_wallet = false;
this.masterFingerprint = false;
} }
allowSend() { allowSend() {
return !!this.use_with_hardware_wallet && this._hdWalletInstance instanceof HDSegwitBech32Wallet && this._hdWalletInstance.allowSend(); return (
this.useWithHardwareWalletEnabled() && this._hdWalletInstance instanceof HDSegwitBech32Wallet && this._hdWalletInstance.allowSend()
);
} }
allowBatchSend() { allowBatchSend() {
return ( return (
!!this.use_with_hardware_wallet && this._hdWalletInstance instanceof HDSegwitBech32Wallet && this._hdWalletInstance.allowBatchSend() this.useWithHardwareWalletEnabled() &&
this._hdWalletInstance instanceof HDSegwitBech32Wallet &&
this._hdWalletInstance.allowBatchSend()
); );
} }
allowSendMax() { allowSendMax() {
return ( return (
!!this.use_with_hardware_wallet && this._hdWalletInstance instanceof HDSegwitBech32Wallet && this._hdWalletInstance.allowSendMax() this.useWithHardwareWalletEnabled() && this._hdWalletInstance instanceof HDSegwitBech32Wallet && this._hdWalletInstance.allowSendMax()
); );
} }
@ -43,7 +48,7 @@ export class WatchOnlyWallet extends LegacyWallet {
try { try {
bitcoin.address.toOutputScript(this.getAddress()); bitcoin.address.toOutputScript(this.getAddress());
return true; return true;
} catch (e) { } catch (_) {
return false; return false;
} }
} }
@ -146,9 +151,43 @@ export class WatchOnlyWallet extends LegacyWallet {
*/ */
createTransaction(utxos, targets, feeRate, changeAddress, sequence) { createTransaction(utxos, targets, feeRate, changeAddress, sequence) {
if (this._hdWalletInstance instanceof HDSegwitBech32Wallet) { 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.getMasterFingerprint());
} else { } else {
throw new Error('Not a zpub watch-only wallet, cant create PSBT (or just not initialized)'); throw new Error('Not a zpub watch-only wallet, cant create PSBT (or just not initialized)');
} }
} }
getMasterFingerprint() {
return this.masterFingerprint;
}
getMasterFingerprintHex() {
if (!this.masterFingerprint) return '00000000';
let masterFingerprintHex = Number(this.masterFingerprint).toString(16);
if (masterFingerprintHex.length < 8) masterFingerprintHex = '0' + masterFingerprintHex; // conversion without explicit zero might result in lost byte
// poor man's little-endian conversion:
// ¯\_(ツ)_/¯
return (
masterFingerprintHex[6] +
masterFingerprintHex[7] +
masterFingerprintHex[4] +
masterFingerprintHex[5] +
masterFingerprintHex[2] +
masterFingerprintHex[3] +
masterFingerprintHex[0] +
masterFingerprintHex[1]
);
}
isHd() {
return this.secret.startsWith('xpub') || this.secret.startsWith('ypub') || this.secret.startsWith('zpub');
}
useWithHardwareWalletEnabled() {
return !!this.use_with_hardware_wallet;
}
setUseWithHardwareWalletEnabled(enabled) {
this.use_with_hardware_wallet = !!enabled;
}
} }

View file

@ -164,6 +164,7 @@
3271B0BA236E329400DA766F /* API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 32F0A2992311DBB20095C559 /* ComplicationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComplicationController.swift; sourceTree = "<group>"; };
@ -332,6 +333,7 @@
13B07FAE1A68108700A75B9A /* BlueWallet */ = { 13B07FAE1A68108700A75B9A /* BlueWallet */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
32C7944323B8879D00BE2AFA /* BlueWalletRelease.entitlements */,
32F0A2502310B0910095C559 /* BlueWallet.entitlements */, 32F0A2502310B0910095C559 /* BlueWallet.entitlements */,
008F07F21AC5B25A0029DE68 /* main.jsbundle */, 008F07F21AC5B25A0029DE68 /* main.jsbundle */,
13B07FAF1A68108700A75B9A /* AppDelegate.h */, 13B07FAF1A68108700A75B9A /* AppDelegate.h */,
@ -681,7 +683,7 @@
attributes = { attributes = {
LastSwiftUpdateCheck = 1120; LastSwiftUpdateCheck = 1120;
LastUpgradeCheck = 1020; LastUpgradeCheck = 1020;
ORGANIZATIONNAME = Facebook; ORGANIZATIONNAME = BlueWallet;
TargetAttributes = { TargetAttributes = {
00E356ED1AD99517003FC87E = { 00E356ED1AD99517003FC87E = {
CreatedOnToolsVersion = 6.2; CreatedOnToolsVersion = 6.2;
@ -1234,7 +1236,7 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEAD_CODE_STRIPPING = NO; DEAD_CODE_STRIPPING = NO;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = A7W54YZ4WU;
HEADER_SEARCH_PATHS = "$(inherited)"; HEADER_SEARCH_PATHS = "$(inherited)";
INFOPLIST_FILE = BlueWallet/Info.plist; INFOPLIST_FILE = BlueWallet/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0; IPHONEOS_DEPLOYMENT_TARGET = 9.0;
@ -1266,11 +1268,11 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = BlueWallet/BlueWallet.entitlements; CODE_SIGN_ENTITLEMENTS = BlueWallet/BlueWalletRelease.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = A7W54YZ4WU;
HEADER_SEARCH_PATHS = "$(inherited)"; HEADER_SEARCH_PATHS = "$(inherited)";
INFOPLIST_FILE = BlueWallet/Info.plist; INFOPLIST_FILE = BlueWallet/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0; IPHONEOS_DEPLOYMENT_TARGET = 9.0;

View file

@ -0,0 +1,20 @@
<?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.developer.ubiquity-kvstore-identifier</key>
<string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string>
<key>com.apple.security.application-groups</key>
<array>
<string>group.io.bluewallet.bluewallet</string>
</array>
</dict>
</plist>

View file

@ -2,12 +2,41 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>en</string> <string>en</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>BlueWallet</string> <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>
<dict>
<key>CFBundleTypeIconFiles</key>
<array/>
<key>CFBundleTypeName</key>
<string>TXN</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSHandlerRank</key>
<string>Owner</string>
<key>LSItemContentTypes</key>
<array>
<string>io.bluewallet.psbt.txn</string>
</array>
</dict>
</array>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
@ -19,7 +48,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>4.9.1</string> <string>5.0.0</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
@ -43,6 +72,8 @@
<false/> <false/>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>NSAppTransportSecurity</key> <key>NSAppTransportSecurity</key>
<dict> <dict>
<key>NSAllowsArbitraryLoads</key> <key>NSAllowsArbitraryLoads</key>
@ -58,18 +89,18 @@
</dict> </dict>
<key>NSAppleMusicUsageDescription</key> <key>NSAppleMusicUsageDescription</key>
<string>This alert should not show up as we do not require this data</string> <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> <key>NSBluetoothPeripheralUsageDescription</key>
<string>This alert should not show up as we do not require this data</string> <string>This alert should not show up as we do not require this data</string>
<key>NSCalendarsUsageDescription</key> <key>NSCalendarsUsageDescription</key>
<string>This alert should not show up as we do not require this data</string> <string>This alert should not show up as we do not require this data</string>
<key>NSCameraUsageDescription</key> <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> <string>In order to quickly scan the recipient's address, we need your permission to use the camera to scan their QR Code.</string>
<key>NSLocationWhenInUseUsageDescription</key> <key>NSFaceIDUsageDescription</key>
<string>This alert should not show up as we do not require this data</string> <string>In order to use FaceID please confirm your permission.</string>
<key>NSLocationAlwaysUsageDescription</key> <key>NSLocationAlwaysUsageDescription</key>
<string>This alert should not show up as we do not require this data</string> <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> <key>NSMicrophoneUsageDescription</key>
<string>This alert should not show up as we do not require this data</string> <string>This alert should not show up as we do not require this data</string>
<key>NSMotionUsageDescription</key> <key>NSMotionUsageDescription</key>
@ -116,7 +147,91 @@
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <string>UIInterfaceOrientationPortraitUpsideDown</string>
</array> </array>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<false/> <false/>
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
</array>
<key>UTTypeDescription</key>
<string>Partially Signed Bitcoin Transaction</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>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
</array>
<key>UTTypeDescription</key>
<string>Bitcoin Transaction</string>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>io.bluewallet.psbt.txn</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>txn</string>
</array>
</dict>
</dict>
</array>
<key>UTImportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
</array>
<key>UTTypeDescription</key>
<string>Partially Signed Bitcoin Transaction</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>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
</array>
<key>UTTypeDescription</key>
<string>Bitcoin Transaction</string>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>io.bluewallet.psbt.txn</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>txn</string>
</array>
</dict>
</dict>
</array>
</dict> </dict>
</plist> </plist>

View file

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>XPC!</string> <string>XPC!</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>4.9.1</string> <string>5.0.0</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>239</string> <string>239</string>
<key>CLKComplicationPrincipalClass</key> <key>CLKComplicationPrincipalClass</key>

View file

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>4.9.1</string> <string>5.0.0</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>239</string> <string>239</string>
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>

View file

@ -76,6 +76,8 @@ PODS:
- React - React
- react-native-camera/RN (3.4.0): - react-native-camera/RN (3.4.0):
- React - React
- react-native-document-picker (3.2.0):
- React
- react-native-haptic-feedback (1.7.1): - react-native-haptic-feedback (1.7.1):
- React - React
- react-native-image-picker (1.1.0): - react-native-image-picker (1.1.0):
@ -169,6 +171,7 @@ DEPENDENCIES:
- react-native-biometrics (from `../node_modules/react-native-biometrics`) - react-native-biometrics (from `../node_modules/react-native-biometrics`)
- "react-native-blur (from `../node_modules/@react-native-community/blur`)" - "react-native-blur (from `../node_modules/@react-native-community/blur`)"
- react-native-camera (from `../node_modules/react-native-camera`) - react-native-camera (from `../node_modules/react-native-camera`)
- react-native-document-picker (from `../node_modules/react-native-document-picker`)
- react-native-haptic-feedback (from `../node_modules/react-native-haptic-feedback`) - react-native-haptic-feedback (from `../node_modules/react-native-haptic-feedback`)
- react-native-image-picker (from `../node_modules/react-native-image-picker`) - react-native-image-picker (from `../node_modules/react-native-image-picker`)
- react-native-randombytes (from `../node_modules/react-native-randombytes`) - react-native-randombytes (from `../node_modules/react-native-randombytes`)
@ -243,6 +246,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/@react-native-community/blur" :path: "../node_modules/@react-native-community/blur"
react-native-camera: react-native-camera:
:path: "../node_modules/react-native-camera" :path: "../node_modules/react-native-camera"
react-native-document-picker:
:path: "../node_modules/react-native-document-picker"
react-native-haptic-feedback: react-native-haptic-feedback:
:path: "../node_modules/react-native-haptic-feedback" :path: "../node_modules/react-native-haptic-feedback"
react-native-image-picker: react-native-image-picker:
@ -331,6 +336,7 @@ SPEC CHECKSUMS:
react-native-biometrics: c892904948a32295b128f633bcc11eda020645c5 react-native-biometrics: c892904948a32295b128f633bcc11eda020645c5
react-native-blur: cad4d93b364f91e7b7931b3fa935455487e5c33c react-native-blur: cad4d93b364f91e7b7931b3fa935455487e5c33c
react-native-camera: 203091b4bf99d48b788a0682ad573e8718724893 react-native-camera: 203091b4bf99d48b788a0682ad573e8718724893
react-native-document-picker: e3516aff0dcf65ee0785d9bcf190eb10e2261154
react-native-haptic-feedback: 22c9dc85fd8059f83bf9edd9212ac4bd4ae6074d react-native-haptic-feedback: 22c9dc85fd8059f83bf9edd9212ac4bd4ae6074d
react-native-image-picker: 3637d63fef7e32a230141ab4660d3ceb773c824f react-native-image-picker: 3637d63fef7e32a230141ab4660d3ceb773c824f
react-native-randombytes: 991545e6eaaf700b4ee384c291ef3d572e0b2ca8 react-native-randombytes: 991545e6eaaf700b4ee384c291ef3d572e0b2ca8

View file

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>4.9.1</string> <string>5.0.0</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1</string> <string>1</string>
<key>NSExtension</key> <key>NSExtension</key>

View file

@ -1,3 +1,48 @@
v5.0.0
======
* ADD: Coldcard support
* FIX: allow capitalized bech32 addresses (closes #838)
* FIX: lnurl scan to receive is not returning the correct view (closes #828)
* FIX: watch-only delete wallet doesnt have confirmation now
* FIX: typo in spanish
v4.9.4
======
* REL: ver bump 4.9.4
* FIX: Lint
* FIX: Listen to lnd invoice changes
* FIX: There's two refresh icons on the main view, when you pull-to-refresh
* FIX: Crash on scan in wallets import when closing the view
* FIX: Allow walet change on invoice creation
* FIX: Allow wallet text input to be empty for new wallet naming
* FIX: LN Invoice amount renders 0
* FIX: Handle both chains
* FIX: deeplinking (safello etc)
* DEL: Remove alerts from main list
v4.9.2
======
* ADD: Swipe to Scan
* ADD: Handle clipboard content with both bitcoin: and lightning:
* ADD: Ask user if they have backed up their seed phrase
* ADD: Export screen allows copying to clipboard if its a LNDHub wallet
* ADD: Show LNDHub backup when creating lnd wallet
* ADD: CLP Fiat
* FIX: TX Time visual glitch
* FIX: Show an alert when theres a fetch transactions error
* FIX: TX list uses whole canvas area
* FIX: Don't allow empty wallet labels
* FIX: Wallet type selecion clipping on advanced mode
* FIX: Receive address was not being rendered
* FIX: Don't show wallet export warning if wallet was imported
* REF: Reworked Import wallet flow
* REF: BIP49 to use electrum
* REF: Custom receive
v4.9.0 v4.9.0
====== ======
@ -11,43 +56,3 @@ v4.9.0
* FIX: LN Scan to receive is more visible * FIX: LN Scan to receive is more visible
* FIX: Quick actions not appearing on non-3d touch devices. * FIX: Quick actions not appearing on non-3d touch devices.
* FIX: Dont show clipboard modal when biometrics is dismissed * FIX: Dont show clipboard modal when biometrics is dismissed
v4.8.1
======
* FIX: Updated biometrics
* FIX: Import QR Code from screenshot not working
v4.8.0
======
* ADD: Today Extension and Quick Actions
* ADD: Send max option on advanced menu
* ADD: Add Onchain address view for Lightning
* FIX: Allow textfield to be visible above keyboard
* FIX: lapp browser when typing URL without https scheme it doesnt work
* ADD: Value and memo to the success screen fix logic for both sent and receive
* FIX: layout for small devices with flexbox
* FIX: Dont allow zero invoices to enable create invoice button
* FIX: Change create button on Receive LN payment should be create invoice
* FIX: Update for watch
v4.7.1
======
* ADD: Lapp browser
* FIX: White screen on boot
* FIX: Lightning wallet was not shown on Watch app
* FIX: crash on PSBT tx broadcast (when using with hardware wallet)
* REF: mnemonic backup screen
* DEL: Auto brightenss
v4.7.0
======
* ADD: external marketplace link
* FIX: electrum connection
* FIX: Now able to use biometrics with encrypted storage (not for unlocking)
* FIX: LApp marketplace address is now editable
* FIX: single address watch-only wallet Receive button crash

View file

@ -25,6 +25,9 @@
latest_transaction: 'laaste transaksie', latest_transaction: 'laaste transaksie',
empty_txs1: 'U transaksies is hier beskikbaar,', empty_txs1: 'U transaksies is hier beskikbaar,',
empty_txs2: 'huidiglik geen transaksies', empty_txs2: 'huidiglik geen transaksies',
empty_txs1_lightning:
'Lightning wallet should be used for your daily transactions. Fees are unfairly cheap and speed is blazing fast.',
empty_txs2_lightning: '\nTo start using it tap on "manage funds" and topup your balance.',
tap_here_to_buy: 'Raak hier om Bitcoin te koop', tap_here_to_buy: 'Raak hier om Bitcoin te koop',
}, },
reorder: { reorder: {
@ -50,6 +53,7 @@
details: { details: {
title: 'Beursiet', title: 'Beursiet',
address: 'AdresAddress', address: 'AdresAddress',
master_fingerprint: 'Master fingerprint',
type: 'Tipe', type: 'Tipe',
label: 'Etiket', label: 'Etiket',
destination: 'bestemming', destination: 'bestemming',
@ -165,6 +169,7 @@
create: 'Skep', create: 'Skep',
setAmount: 'Bedrag ontvang', setAmount: 'Bedrag ontvang',
}, },
scan_lnurl: 'Scan to receive',
}, },
buyBitcoin: { buyBitcoin: {
header: 'Koop Bitcoin', header: 'Koop Bitcoin',
@ -186,10 +191,14 @@
'Om u eie LND node te konnekteer, installeer asseblief LndHub' + 'Om u eie LND node te konnekteer, installeer asseblief LndHub' +
' and put its URL here in settings. Leave blank om die standaard LndHub' + ' and put its URL here in settings. Leave blank om die standaard LndHub' +
'(lndhub.io) te gebruik', '(lndhub.io) te gebruik',
electrum_settings: 'Electrum Settings',
electrum_settings_explain: 'Set to blank to use default',
save: 'stoor', save: 'stoor',
about: 'info', about: 'info',
language: 'Taal', language: 'Taal',
currency: 'Geldeenheid', currency: 'Geldeenheid',
advanced_options: 'Advanced Options',
enable_advanced_mode: 'Enable advanced mode',
}, },
plausibledeniability: { plausibledeniability: {
title: 'Geloofwaardige Ontkenbaarheid', title: 'Geloofwaardige Ontkenbaarheid',

View file

@ -23,6 +23,9 @@
latest_transaction: 'Utshintsho olutsha', latest_transaction: 'Utshintsho olutsha',
empty_txs1: 'Intengiso yakho iya kubonakala apha,', empty_txs1: 'Intengiso yakho iya kubonakala apha,',
empty_txs2: 'akuho nanye okwangoku', empty_txs2: 'akuho nanye okwangoku',
empty_txs1_lightning:
'Lightning wallet should be used for your daily transactions. Fees are unfairly cheap and speed is blazing fast.',
empty_txs2_lightning: '\nTo start using it tap on "manage funds" and topup your balance.',
tap_here_to_buy: 'Cofa apha ukuthenga ibitcoin', tap_here_to_buy: 'Cofa apha ukuthenga ibitcoin',
}, },
reorder: { reorder: {
@ -48,6 +51,7 @@
details: { details: {
title: 'Ingxowa', title: 'Ingxowa',
address: 'Ikheli', address: 'Ikheli',
master_fingerprint: 'Master fingerprint',
type: 'Uhlobo', type: 'Uhlobo',
label: 'Igama', label: 'Igama',
destination: 'ukuya kuyo', destination: 'ukuya kuyo',
@ -163,6 +167,7 @@
create: 'Yenza', create: 'Yenza',
setAmount: 'Fumana ngexabiso', setAmount: 'Fumana ngexabiso',
}, },
scan_lnurl: 'Scan to receive',
}, },
buyBitcoin: { buyBitcoin: {
header: 'Thenga Ibitcoin', header: 'Thenga Ibitcoin',
@ -183,10 +188,14 @@
lightning_settings_explain: lightning_settings_explain:
'Ukuxhuma kwi-node yakho ye-LND nceda ufake iLndHub' + 'Ukuxhuma kwi-node yakho ye-LND nceda ufake iLndHub' +
' kwaye ufake iURL apha izicwangciso. Shiya kungenanto yokusebenzisa iLndHub (Indhub.io)', ' kwaye ufake iURL apha izicwangciso. Shiya kungenanto yokusebenzisa iLndHub (Indhub.io)',
electrum_settings: 'Electrum Settings',
electrum_settings_explain: 'Set to blank to use default',
save: 'ndoloza', save: 'ndoloza',
about: 'Malunga', about: 'Malunga',
language: 'Ulwimi', language: 'Ulwimi',
currency: 'Lwemali', currency: 'Lwemali',
advanced_options: 'Advanced Options',
enable_advanced_mode: 'Enable advanced mode',
}, },
plausibledeniability: { plausibledeniability: {
title: 'Ukuphika', title: 'Ukuphika',

View file

@ -50,6 +50,7 @@ module.exports = {
details: { details: {
title: 'Peněženka', title: 'Peněženka',
address: 'Adresa', address: 'Adresa',
master_fingerprint: 'Master fingerprint',
type: 'Typ', type: 'Typ',
label: 'Popisek', label: 'Popisek',
destination: 'cíl', destination: 'cíl',

View file

@ -50,6 +50,7 @@ module.exports = {
details: { details: {
title: 'Wallet', title: 'Wallet',
address: 'Adresse', address: 'Adresse',
master_fingerprint: 'Master fingerprint',
type: 'Type', type: 'Type',
label: 'Etiket', label: 'Etiket',
destination: 'destination', destination: 'destination',
@ -223,6 +224,8 @@ module.exports = {
refill_lnd_balance: 'Genopfyld Lightning wallet', refill_lnd_balance: 'Genopfyld Lightning wallet',
refill: 'Genopfyld', refill: 'Genopfyld',
withdraw: 'Træk coins tilbage', withdraw: 'Træk coins tilbage',
expired: 'Expired',
sameWalletAsInvoiceError: 'You can not pay an invoice with the same wallet used to create it.',
}, },
pleasebackup: { pleasebackup: {
title: 'Your wallet is created...', title: 'Your wallet is created...',

View file

@ -52,6 +52,7 @@ module.exports = {
details: { details: {
title: 'Wallet', title: 'Wallet',
address: 'Adresse', address: 'Adresse',
master_fingerprint: 'Master fingerprint',
type: 'Typ', type: 'Typ',
label: 'Bezeichnung', label: 'Bezeichnung',
destination: 'Zieladresse', destination: 'Zieladresse',
@ -225,6 +226,7 @@ module.exports = {
refill_lnd_balance: 'Lade deine Lightning Wallet auf', refill_lnd_balance: 'Lade deine Lightning Wallet auf',
refill: 'Aufladen', refill: 'Aufladen',
withdraw: 'Abheben', withdraw: 'Abheben',
expired: 'Expired',
placeholder: 'Invoice', placeholder: 'Invoice',
sameWalletAsInvoiceError: sameWalletAsInvoiceError:
'Du kannst nicht die Rechnung mit der Wallet begleichen, die du für die Erstellung dieser Rechnung verwendet hast.', 'Du kannst nicht die Rechnung mit der Wallet begleichen, die du für die Erstellung dieser Rechnung verwendet hast.',

View file

@ -53,6 +53,7 @@ module.exports = {
details: { details: {
title: 'Πορτοφόλι', title: 'Πορτοφόλι',
address: 'Διεύθυνση', address: 'Διεύθυνση',
master_fingerprint: 'Master fingerprint',
type: 'Τύπος', type: 'Τύπος',
label: 'Ετικέτα', label: 'Ετικέτα',
destination: 'προορισμός', destination: 'προορισμός',

View file

@ -51,6 +51,7 @@ module.exports = {
details: { details: {
title: 'Wallet', title: 'Wallet',
address: 'Address', address: 'Address',
master_fingerprint: 'Master fingerprint',
type: 'Type', type: 'Type',
label: 'Label', label: 'Label',
destination: 'destination', destination: 'destination',
@ -80,7 +81,7 @@ module.exports = {
error: 'Failed to import. Please, make sure that the provided data is valid.', error: 'Failed to import. Please, make sure that the provided data is valid.',
success: 'Success', success: 'Success',
do_import: 'Import', do_import: 'Import',
scan_qr: 'or scan QR code instead?', scan_qr: '...scan QR or import file instead?',
}, },
scanQrWif: { scanQrWif: {
go_back: 'Go Back', go_back: 'Go Back',
@ -145,7 +146,7 @@ module.exports = {
title: 'create transaction', title: 'create transaction',
error: 'Error creating transaction. Invalid address or send amount?', error: 'Error creating transaction. Invalid address or send amount?',
go_back: 'Go Back', go_back: 'Go Back',
this_is_hex: 'This is transaction hex, signed and ready to be broadcast to the network.', this_is_hex: `This is your transaction's hex, signed and ready to be broadcasted to the network.`,
to: 'To', to: 'To',
amount: 'Amount', amount: 'Amount',
fee: 'Fee', fee: 'Fee',

View file

@ -35,7 +35,7 @@ module.exports = {
title: 'Añadir billetera', title: 'Añadir billetera',
description: description:
'Puedes escanear la billetera de papel (en WIF - Formato de importación de billeteras) o crear una nueva billetera. Las billeteras SegWit estan compatibles por defecto.', 'Puedes escanear la billetera de papel (en WIF - Formato de importación de billeteras) o crear una nueva billetera. Las billeteras SegWit estan compatibles por defecto.',
scan: 'Escaniar', scan: 'Escanear',
create: 'Crear', create: 'Crear',
label_new_segwit: 'Nuevo SegWit', label_new_segwit: 'Nuevo SegWit',
label_new_lightning: 'Nuevo Lightning', label_new_lightning: 'Nuevo Lightning',
@ -51,6 +51,7 @@ module.exports = {
details: { details: {
title: 'Detalles de la billetera', title: 'Detalles de la billetera',
address: 'Dirección', address: 'Dirección',
master_fingerprint: 'Master fingerprint',
type: 'Tipo', type: 'Tipo',
label: 'Etiqueta', label: 'Etiqueta',
delete: 'Eliminar', delete: 'Eliminar',

View file

@ -53,6 +53,7 @@ module.exports = {
details: { details: {
title: 'Lompakko', title: 'Lompakko',
address: 'Osoite', address: 'Osoite',
master_fingerprint: 'Master fingerprint',
type: 'Tyyppi', type: 'Tyyppi',
label: 'Etiketti', label: 'Etiketti',
destination: 'määränpää', destination: 'määränpää',

View file

@ -52,6 +52,7 @@ module.exports = {
details: { details: {
title: 'Portefeuille', title: 'Portefeuille',
address: 'Adresse', address: 'Adresse',
master_fingerprint: 'Master fingerprint',
type: 'Type', type: 'Type',
label: 'Libelé', label: 'Libelé',
destination: 'destination', destination: 'destination',
@ -146,7 +147,7 @@ module.exports = {
title: 'créer une transaction', title: 'créer une transaction',
error: 'Erreur creating transaction. Invalid address or send amount?', error: 'Erreur creating transaction. Invalid address or send amount?',
go_back: 'Retour', go_back: 'Retour',
this_is_hex: 'This is transaction hex, signed and ready to be broadcast to the network.', this_is_hex: `This is your transaction's hex, signed and ready to be broadcasted to the network.`,
to: 'À', to: 'À',
amount: 'Montant', amount: 'Montant',
fee: 'Frais', fee: 'Frais',

View file

@ -10,6 +10,8 @@ module.exports = {
wallets: { wallets: {
select_wallet: 'Odaberi volet', select_wallet: 'Odaberi volet',
options: 'opcije', options: 'opcije',
createBitcoinWallet:
'You currently do not have a Bitcoin wallet. In order to fund a Lightning wallet, a Bitcoin wallet needs to be created or imported. Would you like to continue anyway?',
list: { list: {
app_name: 'BlueWallet', app_name: 'BlueWallet',
title: 'Voleti', title: 'Voleti',
@ -48,6 +50,7 @@ module.exports = {
details: { details: {
title: 'Volet', title: 'Volet',
address: 'Adresa', address: 'Adresa',
master_fingerprint: 'Master fingerprint',
type: 'Tip', type: 'Tip',
label: 'Oznaka', label: 'Oznaka',
destination: 'odredište', destination: 'odredište',

View file

@ -51,8 +51,10 @@ module.exports = {
details: { details: {
title: 'Tárca', title: 'Tárca',
address: 'Cím', address: 'Cím',
master_fingerprint: 'Master fingerprint',
type: 'Típus', type: 'Típus',
label: 'Cimke', label: 'Cimke',
destination: 'destination',
description: 'leírás', description: 'leírás',
are_you_sure: 'Biztos vagy benne?', are_you_sure: 'Biztos vagy benne?',
yes_delete: 'Igen, töröld', yes_delete: 'Igen, töröld',
@ -187,6 +189,8 @@ module.exports = {
'Saját LND-csomóponthoz való csatlakozáshoz telepítsd az LndHub-ot' + 'Saját LND-csomóponthoz való csatlakozáshoz telepítsd az LndHub-ot' +
' és írd be az URL-ét alul. Hagyd üresen, ha a BlueWallet saját LNDHub-jához (lndhub.io) szeretnél csatlakozni.' + ' és írd be az URL-ét alul. Hagyd üresen, ha a BlueWallet saját LNDHub-jához (lndhub.io) szeretnél csatlakozni.' +
' A beállítások mentése után, minden újonnan létrehozott tárca a megadott LDNHubot fogja használni.', ' A beállítások mentése után, minden újonnan létrehozott tárca a megadott LDNHubot fogja használni.',
electrum_settings: 'Electrum Settings',
electrum_settings_explain: 'Set to blank to use default',
save: 'Ment', save: 'Ment',
about: 'Egyéb', about: 'Egyéb',
language: 'Nyelv', language: 'Nyelv',

View file

@ -51,6 +51,7 @@ module.exports = {
details: { details: {
title: 'Dompet', title: 'Dompet',
address: 'Alamat', address: 'Alamat',
master_fingerprint: 'Master fingerprint',
type: 'Tipe', type: 'Tipe',
label: 'Label', label: 'Label',
destination: 'tujuan', destination: 'tujuan',

View file

@ -53,6 +53,7 @@ module.exports = {
details: { details: {
title: 'Portafoglio', title: 'Portafoglio',
address: 'Indirizzo', address: 'Indirizzo',
master_fingerprint: 'Master fingerprint',
type: 'Tipo', type: 'Tipo',
label: 'Etichetta', label: 'Etichetta',
destination: 'Destinazione', destination: 'Destinazione',

View file

@ -50,6 +50,7 @@ module.exports = {
details: { details: {
title: 'ウォレット', title: 'ウォレット',
address: 'アドレス', address: 'アドレス',
master_fingerprint: 'Master fingerprint',
type: 'タイプ', type: 'タイプ',
label: 'ラベル', label: 'ラベル',
destination: '送り先', destination: '送り先',

View file

@ -51,6 +51,7 @@ module.exports = {
details: { details: {
title: 'Lommebok', title: 'Lommebok',
address: 'Adresse', address: 'Adresse',
master_fingerprint: 'Master fingerprint',
type: 'Type', type: 'Type',
label: 'Merkelapp', label: 'Merkelapp',
destination: 'mål', destination: 'mål',

View file

@ -51,6 +51,7 @@ module.exports = {
details: { details: {
title: 'Portemonnee', title: 'Portemonnee',
address: 'Adres', address: 'Adres',
master_fingerprint: 'Master fingerprint',
type: 'Type', type: 'Type',
label: 'Label', label: 'Label',
destination: 'bestemming', destination: 'bestemming',

View file

@ -53,6 +53,7 @@ module.exports = {
details: { details: {
title: 'Carteira', title: 'Carteira',
address: 'Endereço', address: 'Endereço',
master_fingerprint: 'Master fingerprint',
type: 'Tipo', type: 'Tipo',
destination: 'destino', destination: 'destino',
description: 'descrição', description: 'descrição',

View file

@ -51,6 +51,7 @@ module.exports = {
details: { details: {
title: 'wallet', title: 'wallet',
address: 'Endereço', address: 'Endereço',
master_fingerprint: 'Master fingerprint',
type: 'Tipo', type: 'Tipo',
delete: 'Eliminar', delete: 'Eliminar',
save: 'Guardar', save: 'Guardar',

View file

@ -51,6 +51,7 @@ module.exports = {
details: { details: {
title: 'Информация о кошельке', title: 'Информация о кошельке',
address: 'Адрес', address: 'Адрес',
master_fingerprint: 'Master fingerprint',
type: 'Тип', type: 'Тип',
label: 'Метка', label: 'Метка',
delete: 'Удалить', delete: 'Удалить',

View file

@ -51,8 +51,10 @@ module.exports = {
details: { details: {
title: 'Plånbok', title: 'Plånbok',
address: 'Adress', address: 'Adress',
master_fingerprint: 'Master fingerprint',
type: 'Typ', type: 'Typ',
label: 'Etikett', label: 'Etikett',
destination: 'destination',
description: 'beskrivning', description: 'beskrivning',
are_you_sure: 'Är du säker?', are_you_sure: 'Är du säker?',
yes_delete: 'Ja, ta bort', yes_delete: 'Ja, ta bort',

View file

@ -50,6 +50,7 @@ module.exports = {
details: { details: {
title: 'กระเป๋าสตางค์', title: 'กระเป๋าสตางค์',
address: 'แอดเดรส', address: 'แอดเดรส',
master_fingerprint: 'Master fingerprint',
type: 'ชนิด', type: 'ชนิด',
label: 'ป้าย', label: 'ป้าย',
destination: 'เป้าหมาย', destination: 'เป้าหมาย',

View file

@ -51,6 +51,7 @@ module.exports = {
details: { details: {
title: 'Cüzdan', title: 'Cüzdan',
address: 'Adres', address: 'Adres',
master_fingerprint: 'Master fingerprint',
type: 'Tip', type: 'Tip',
label: 'Etiket', label: 'Etiket',
destination: 'hedef', destination: 'hedef',

View file

@ -51,6 +51,7 @@ module.exports = {
details: { details: {
title: 'Інформація про Гаманець', title: 'Інформація про Гаманець',
address: 'Адреса', address: 'Адреса',
master_fingerprint: 'Master fingerprint',
type: 'Тип', type: 'Тип',
delete: 'Delete', delete: 'Delete',
save: 'Save', save: 'Save',

View file

@ -51,6 +51,7 @@ module.exports = {
details: { details: {
title: 'Wallet', title: 'Wallet',
address: 'Address', address: 'Address',
master_fingerprint: 'Master fingerprint',
type: 'Type', type: 'Type',
label: 'Label', label: 'Label',
destination: 'destination', destination: 'destination',
@ -145,7 +146,7 @@ module.exports = {
title: 'create transaction', title: 'create transaction',
error: 'Error creating transaction. Invalid address or send amount?', error: 'Error creating transaction. Invalid address or send amount?',
go_back: 'Go Back', go_back: 'Go Back',
this_is_hex: 'This is transaction hex, signed and ready to be broadcast to the network.', this_is_hex: `This is your transaction's hex, signed and ready to be broadcasted to the network.`,
to: 'To', to: 'To',
amount: 'Amount', amount: 'Amount',
fee: 'Fee', fee: 'Fee',

View file

@ -49,6 +49,7 @@ module.exports = {
details: { details: {
title: '钱包', title: '钱包',
address: '地址', address: '地址',
master_fingerprint: 'Master fingerprint',
type: '类型', type: '类型',
label: '标签', label: '标签',
destination: '目的', destination: '目的',
@ -219,6 +220,7 @@ module.exports = {
withdraw: '提取', withdraw: '提取',
expired: '超时', expired: '超时',
sameWalletAsInvoiceError: '你不能用创建账单的钱包去支付该账单', sameWalletAsInvoiceError: '你不能用创建账单的钱包去支付该账单',
placeholder: 'Invoice',
}, },
pleasebackup: { pleasebackup: {
title: 'Your wallet is created...', title: 'Your wallet is created...',

View file

@ -49,6 +49,7 @@ module.exports = {
details: { details: {
title: '錢包', title: '錢包',
address: '地址', address: '地址',
master_fingerprint: 'Master fingerprint',
type: '類型', type: '類型',
label: '標籤', label: '標籤',
destination: '目的', destination: '目的',
@ -163,6 +164,7 @@ module.exports = {
create: '建立', create: '建立',
setAmount: '收款金額', setAmount: '收款金額',
}, },
scan_lnurl: 'Scan to receive',
}, },
buyBitcoin: { buyBitcoin: {
header: '購買比特幣', header: '購買比特幣',
@ -181,6 +183,8 @@ module.exports = {
encrypt_storage: '加密儲存', encrypt_storage: '加密儲存',
lightning_settings: '閃電網路設定', lightning_settings: '閃電網路設定',
lightning_settings_explain: '如要要連線你自己的閃電節點請安裝LndHub' + ' 並把url地址輸入到下面. 空白將使用預設的LndHub (lndhub.io)', lightning_settings_explain: '如要要連線你自己的閃電節點請安裝LndHub' + ' 並把url地址輸入到下面. 空白將使用預設的LndHub (lndhub.io)',
electrum_settings: 'Electrum Settings',
electrum_settings_explain: 'Set to blank to use default',
save: '儲存', save: '儲存',
about: '關於', about: '關於',
language: '語言', language: '語言',
@ -215,6 +219,7 @@ module.exports = {
refill: '充值', refill: '充值',
withdraw: '提取', withdraw: '提取',
expired: '超時', expired: '超時',
placeholder: 'Invoice',
sameWalletAsInvoiceError: '你不能用建立賬單的錢包去支付該賬單', sameWalletAsInvoiceError: '你不能用建立賬單的錢包去支付該賬單',
}, },
pleasebackup: { pleasebackup: {

584
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "BlueWallet", "name": "BlueWallet",
"version": "4.9.1", "version": "5.0.0",
"devDependencies": { "devDependencies": {
"@babel/core": "^7.5.0", "@babel/core": "^7.5.0",
"@babel/runtime": "^7.5.1", "@babel/runtime": "^7.5.1",
@ -38,7 +38,8 @@
"postinstall": "./node_modules/.bin/rn-nodeify --install buffer,events,process,stream,util,inherits,fs,path --hack; npm run releasenotes2json; npm run podinstall; npx jetify", "postinstall": "./node_modules/.bin/rn-nodeify --install buffer,events,process,stream,util,inherits,fs,path --hack; npm run releasenotes2json; npm run podinstall; npx jetify",
"test": "npm run unit && npm run jest && npm run lint", "test": "npm run unit && npm run jest && npm run lint",
"jest": "node node_modules/jest/bin/jest.js tests/integration/*", "jest": "node node_modules/jest/bin/jest.js tests/integration/*",
"lint": "./node_modules/.bin/eslint *.js screen/**/*.js screen/ class/ models/ loc/ tests/integration/ --fix", "lint": "./node_modules/.bin/eslint *.js screen/**/*.js screen/ class/ models/ loc/ tests/integration/",
"lint:fix": "./node_modules/.bin/eslint *.js screen/**/*.js screen/ class/ models/ loc/ tests/integration/ --fix",
"unit": "./node_modules/.bin/mocha tests/unit/*" "unit": "./node_modules/.bin/mocha tests/unit/*"
}, },
"jest": { "jest": {
@ -64,6 +65,7 @@
"bip32": "2.0.3", "bip32": "2.0.3",
"bip39": "2.5.0", "bip39": "2.5.0",
"bitcoinjs-lib": "5.1.6", "bitcoinjs-lib": "5.1.6",
"bolt11": "1.2.7",
"buffer": "5.2.1", "buffer": "5.2.1",
"buffer-reverse": "1.0.1", "buffer-reverse": "1.0.1",
"coinselect": "3.1.11", "coinselect": "3.1.11",
@ -71,7 +73,7 @@
"dayjs": "1.8.14", "dayjs": "1.8.14",
"ecurve": "1.0.6", "ecurve": "1.0.6",
"electrum-client": "git+https://github.com/BlueWallet/rn-electrum-client.git", "electrum-client": "git+https://github.com/BlueWallet/rn-electrum-client.git",
"eslint-config-prettier": "6.0.0", "eslint-config-prettier": "6.10.0",
"eslint-config-standard": "12.0.0", "eslint-config-standard": "12.0.0",
"eslint-config-standard-react": "7.0.2", "eslint-config-standard-react": "7.0.2",
"eslint-plugin-prettier": "3.1.0", "eslint-plugin-prettier": "3.1.0",
@ -93,6 +95,7 @@
"react-native-camera": "3.4.0", "react-native-camera": "3.4.0",
"react-native-default-preference": "1.4.1", "react-native-default-preference": "1.4.1",
"react-native-device-info": "4.0.1", "react-native-device-info": "4.0.1",
"react-native-document-picker": "git+https://github.com/BlueWallet/react-native-document-picker.git#9ce83792db340d01b1361d24b19613658abef4aa",
"react-native-elements": "0.19.0", "react-native-elements": "0.19.0",
"react-native-flexi-radio-button": "0.2.2", "react-native-flexi-radio-button": "0.2.2",
"react-native-fs": "2.13.3", "react-native-fs": "2.13.3",

View file

@ -20,7 +20,7 @@ import {
import { LightningCustodianWallet } from '../../class/lightning-custodian-wallet'; import { LightningCustodianWallet } from '../../class/lightning-custodian-wallet';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import bech32 from 'bech32'; import bech32 from 'bech32';
import { BitcoinUnit } from '../../models/bitcoinUnits'; import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import NavigationService from '../../NavigationService'; import NavigationService from '../../NavigationService';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import { Icon } from 'react-native-elements'; import { Icon } from 'react-native-elements';
@ -36,7 +36,8 @@ export default class LNDCreateInvoice extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this._keyboardDidShow);
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this._keyboardDidHide);
let fromWallet; let fromWallet;
if (props.navigation.state.params.fromWallet) fromWallet = props.navigation.getParam('fromWallet'); if (props.navigation.state.params.fromWallet) fromWallet = props.navigation.getParam('fromWallet');
@ -56,6 +57,7 @@ export default class LNDCreateInvoice extends Component {
lnurl: '', lnurl: '',
lnurlParams: null, lnurlParams: null,
isLoading: true, isLoading: true,
renderWalletSelectionButtonHidden: false,
}; };
} }
@ -85,6 +87,19 @@ export default class LNDCreateInvoice extends Component {
} }
} }
componentWillUnmount() {
this.keyboardDidShowListener.remove();
this.keyboardDidHideListener.remove();
}
_keyboardDidShow = () => {
this.setState({ renderWalletSelectionButtonHidden: true });
};
_keyboardDidHide = () => {
this.setState({ renderWalletSelectionButtonHidden: false });
};
async createInvoice() { async createInvoice() {
this.setState({ isLoading: true }, async () => { this.setState({ isLoading: true }, async () => {
try { try {
@ -195,7 +210,10 @@ export default class LNDCreateInvoice extends Component {
<TouchableOpacity <TouchableOpacity
disabled={this.state.isLoading} disabled={this.state.isLoading}
onPress={() => { onPress={() => {
NavigationService.navigate('ScanQrAddress', { onBarScanned: this.processLnurl }); NavigationService.navigate('ScanQrAddress', {
onBarScanned: this.processLnurl,
launchedBy: this.props.navigation.state.routeName,
});
Keyboard.dismiss(); Keyboard.dismiss();
}} }}
style={{ style={{
@ -216,6 +234,45 @@ export default class LNDCreateInvoice extends Component {
); );
}; };
renderWalletSelectionButton = () => {
if (this.state.renderWalletSelectionButtonHidden) return;
return (
<View style={{ marginBottom: 16, alignItems: 'center', justifyContent: 'center' }}>
{!this.state.isLoading && (
<TouchableOpacity
style={{ flexDirection: 'row', alignItems: 'center' }}
onPress={() =>
this.props.navigation.navigate('SelectWallet', { onWalletSelect: this.onWalletSelect, chainType: Chain.OFFCHAIN })
}
>
<Text style={{ color: '#9aa0aa', fontSize: 14, marginRight: 8 }}>{loc.wallets.select_wallet.toLowerCase()}</Text>
<Icon name="angle-right" size={18} type="font-awesome" color="#9aa0aa" />
</TouchableOpacity>
)}
<View style={{ flexDirection: 'row', alignItems: 'center', marginVertical: 4 }}>
<TouchableOpacity
style={{ flexDirection: 'row', alignItems: 'center' }}
onPress={() =>
this.props.navigation.navigate('SelectWallet', { onWalletSelect: this.onWalletSelect, chainType: Chain.OFFCHAIN })
}
>
<Text style={{ color: '#0c2550', fontSize: 14 }}>{this.state.fromWallet.getLabel()}</Text>
<Text style={{ color: '#0c2550', fontSize: 14, fontWeight: '600', marginLeft: 8, marginRight: 4 }}>
{loc.formatBalanceWithoutSuffix(this.state.fromWallet.getBalance(), BitcoinUnit.SATS, false)}
</Text>
<Text style={{ color: '#0c2550', fontSize: 11, fontWeight: '600', textAlignVertical: 'bottom', marginTop: 2 }}>
{BitcoinUnit.SATS}
</Text>
</TouchableOpacity>
</View>
</View>
);
};
onWalletSelect = wallet => {
this.setState({ fromWallet: wallet }, () => this.props.navigation.pop());
};
render() { render() {
if (!this.state.fromWallet) { if (!this.state.fromWallet) {
return ( return (
@ -283,6 +340,7 @@ export default class LNDCreateInvoice extends Component {
{this.renderCreateButton()} {this.renderCreateButton()}
</KeyboardAvoidingView> </KeyboardAvoidingView>
</View> </View>
{this.renderWalletSelectionButton()}
</View> </View>
</TouchableWithoutFeedback> </TouchableWithoutFeedback>
); );
@ -295,7 +353,9 @@ LNDCreateInvoice.propTypes = {
dismiss: PropTypes.func, dismiss: PropTypes.func,
navigate: PropTypes.func, navigate: PropTypes.func,
getParam: PropTypes.func, getParam: PropTypes.func,
pop: PropTypes.func,
state: PropTypes.shape({ state: PropTypes.shape({
routeName: PropTypes.string,
params: PropTypes.shape({ params: PropTypes.shape({
uri: PropTypes.string, uri: PropTypes.string,
fromWallet: PropTypes.shape({}), fromWallet: PropTypes.shape({}),

View file

@ -80,10 +80,51 @@ export default class ScanLndInvoice extends React.Component {
} }
} }
componentDidMount() { static getDerivedStateFromProps(props, state) {
if (this.props.navigation.state.params.uri) { if (props.navigation.state.params.uri) {
this.processTextForInvoice(this.props.navigation.getParam('uri')); let data = props.navigation.state.params.uri;
// handling BIP21 w/BOLT11 support
let ind = data.indexOf('lightning=');
if (ind !== -1) {
data = data.substring(ind + 10).split('&')[0];
}
data = data.replace('LIGHTNING:', '').replace('lightning:', '');
console.log(data);
/**
* @type {LightningCustodianWallet}
*/
let w = state.fromWallet;
let decoded;
try {
decoded = w.decodeInvoice(data);
let expiresIn = (decoded.timestamp * 1 + decoded.expiry * 1) * 1000; // ms
if (+new Date() > expiresIn) {
expiresIn = 'expired';
} else {
expiresIn = Math.round((expiresIn - +new Date()) / (60 * 1000)) + ' min';
}
Keyboard.dismiss();
props.navigation.setParams({ uri: undefined });
return {
invoice: data,
decoded,
expiresIn,
destination: data,
isAmountInitiallyEmpty: decoded.num_satoshis === '0',
isLoading: false,
};
} catch (Err) {
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
Keyboard.dismiss();
props.navigation.setParams({ uri: undefined });
setTimeout(() => alert(Err.message), 10);
return { ...state, isLoading: false };
}
} }
return state;
} }
componentWillUnmount() { componentWillUnmount() {
@ -100,52 +141,7 @@ export default class ScanLndInvoice extends React.Component {
}; };
processInvoice = data => { processInvoice = data => {
this.setState({ isLoading: true }, async () => { this.props.navigation.setParams({ uri: data });
if (!this.state.fromWallet) {
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
alert('Before paying a Lightning invoice, you must first add a Lightning wallet.');
return this.props.navigation.goBack();
}
// handling BIP21 w/BOLT11 support
let ind = data.indexOf('lightning=');
if (ind !== -1) {
data = data.substring(ind + 10).split('&')[0];
}
data = data.replace('LIGHTNING:', '').replace('lightning:', '');
console.log(data);
/**
* @type {LightningCustodianWallet}
*/
let w = this.state.fromWallet;
let decoded;
try {
decoded = await w.decodeInvoice(data);
let expiresIn = (decoded.timestamp * 1 + decoded.expiry * 1) * 1000; // ms
if (+new Date() > expiresIn) {
expiresIn = 'expired';
} else {
expiresIn = Math.round((expiresIn - +new Date()) / (60 * 1000)) + ' min';
}
Keyboard.dismiss();
this.setState({
invoice: data,
decoded,
expiresIn,
destination: data,
isAmountInitiallyEmpty: decoded.num_satoshis === '0',
isLoading: false,
});
} catch (Err) {
Keyboard.dismiss();
this.setState({ isLoading: false });
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
alert(Err.message);
}
});
}; };
async pay() { async pay() {
@ -216,7 +212,7 @@ export default class ScanLndInvoice extends React.Component {
if (typeof this.state.decoded !== 'object') { if (typeof this.state.decoded !== 'object') {
return true; return true;
} else { } else {
if (!this.state.decoded.hasOwnProperty('num_satoshis')) { if (!this.state.decoded.num_satoshis) {
return true; return true;
} }
} }
@ -295,7 +291,7 @@ export default class ScanLndInvoice extends React.Component {
<BlueCard> <BlueCard>
<BlueAddressInput <BlueAddressInput
onChangeText={text => { onChangeText={text => {
this.setState({ destination: text }); text = text.trim();
this.processTextForInvoice(text); this.processTextForInvoice(text);
}} }}
onBarScanned={this.processInvoice} onBarScanned={this.processInvoice}
@ -355,6 +351,7 @@ ScanLndInvoice.propTypes = {
navigate: PropTypes.func, navigate: PropTypes.func,
pop: PropTypes.func, pop: PropTypes.func,
getParam: PropTypes.func, getParam: PropTypes.func,
setParams: PropTypes.func,
dismiss: PropTypes.func, dismiss: PropTypes.func,
state: PropTypes.shape({ state: PropTypes.shape({
routeName: PropTypes.string, routeName: PropTypes.string,

View file

@ -1,3 +1,4 @@
/* global alert */
import React, { Component } from 'react'; import React, { Component } from 'react';
import { import {
TextInput, TextInput,
@ -11,26 +12,36 @@ import {
Keyboard, Keyboard,
Text, Text,
View, View,
Platform,
PermissionsAndroid,
} from 'react-native'; } from 'react-native';
import { BlueNavigationStyle, SafeBlueArea, BlueCard, BlueText } from '../../BlueComponents'; import { BlueNavigationStyle, SafeBlueArea, BlueCard, BlueText } from '../../BlueComponents';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Privacy from '../../Privacy'; import Privacy from '../../Privacy';
import { BitcoinUnit } from '../../models/bitcoinUnits'; import { BitcoinUnit } from '../../models/bitcoinUnits';
import { Icon } from 'react-native-elements';
import Share from 'react-native-share';
import RNFS from 'react-native-fs';
/** @type {AppStorage} */ /** @type {AppStorage} */
const BlueApp = require('../../BlueApp'); const BlueApp = require('../../BlueApp');
const loc = require('../../loc'); const loc = require('../../loc');
const currency = require('../../currency'); const currency = require('../../currency');
export default class SendCreate extends Component { export default class SendCreate extends Component {
static navigationOptions = () => ({ static navigationOptions = ({ navigation }) => ({
...BlueNavigationStyle, ...BlueNavigationStyle,
title: loc.send.create.details, title: loc.send.create.details,
headerRight: navigation.state.params.exportTXN ? (
<TouchableOpacity style={{ marginRight: 16 }} onPress={navigation.state.params.exportTXN}>
<Icon size={22} name="share-alternative" type="entypo" color={BlueApp.settings.foregroundColor} />
</TouchableOpacity>
) : null,
}); });
constructor(props) { constructor(props) {
super(props); super(props);
console.log('send/create constructor'); console.log('send/create constructor');
props.navigation.setParams({ exportTXN: this.exportTXN });
this.state = { this.state = {
isLoading: false, isLoading: false,
fee: props.navigation.getParam('fee'), fee: props.navigation.getParam('fee'),
@ -44,11 +55,43 @@ export default class SendCreate extends Component {
}; };
} }
async componentDidMount() { componentDidMount() {
Privacy.enableBlur(); Privacy.enableBlur();
console.log('send/create - componentDidMount'); console.log('send/create - componentDidMount');
} }
exportTXN = async () => {
const fileName = `${Date.now()}.txn`;
if (Platform.OS === 'ios') {
const filePath = RNFS.TemporaryDirectoryPath + `/${fileName}`;
await RNFS.writeFile(filePath, this.state.tx);
Share.open({
url: 'file://' + filePath,
})
.catch(error => console.log(error))
.finally(() => {
RNFS.unlink(filePath);
});
} else if (Platform.OS === 'android') {
const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, {
title: 'BlueWallet Storage Access Permission',
message: 'BlueWallet needs your permission to access your storage to save this transaction.',
buttonNeutral: 'Ask Me Later',
buttonNegative: 'Cancel',
buttonPositive: 'OK',
});
if (granted === PermissionsAndroid.RESULTS.GRANTED) {
console.log('Storage Permission: Granted');
const filePath = RNFS.ExternalCachesDirectoryPath + `/${this.fileName}`;
await RNFS.writeFile(filePath, this.state.tx);
alert(`This transaction has been saved in ${filePath}`);
} else {
console.log('Storage Permission: Denied');
}
}
};
componentWillUnmount() { componentWillUnmount() {
Privacy.disableBlur(); Privacy.disableBlur();
} }
@ -164,6 +207,7 @@ const styles = StyleSheet.create({
SendCreate.propTypes = { SendCreate.propTypes = {
navigation: PropTypes.shape({ navigation: PropTypes.shape({
goBack: PropTypes.func, goBack: PropTypes.func,
setParams: PropTypes.func,
getParam: PropTypes.func, getParam: PropTypes.func,
navigate: PropTypes.func, navigate: PropTypes.func,
dismiss: PropTypes.func, dismiss: PropTypes.func,

View file

@ -38,6 +38,9 @@ import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import { HDLegacyP2PKHWallet, HDSegwitBech32Wallet, HDSegwitP2SHWallet, LightningCustodianWallet, WatchOnlyWallet } from '../../class'; import { HDLegacyP2PKHWallet, HDSegwitBech32Wallet, HDSegwitP2SHWallet, LightningCustodianWallet, WatchOnlyWallet } from '../../class';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import { BitcoinTransaction } from '../../models/bitcoinTransactionInfo'; import { BitcoinTransaction } from '../../models/bitcoinTransactionInfo';
import DocumentPicker from 'react-native-document-picker';
import RNFS from 'react-native-fs';
import DeeplinkSchemaMatch from '../../class/deeplinkSchemaMatch';
const bitcoin = require('bitcoinjs-lib'); const bitcoin = require('bitcoinjs-lib');
const bip21 = require('bip21'); const bip21 = require('bip21');
let BigNumber = require('bignumber.js'); let BigNumber = require('bignumber.js');
@ -135,7 +138,10 @@ export default class SendDetails extends Component {
} else { } else {
let recipients = this.state.addresses; let recipients = this.state.addresses;
const dataWithoutSchema = data.replace('bitcoin:', ''); const dataWithoutSchema = data.replace('bitcoin:', '');
if (btcAddressRx.test(dataWithoutSchema) || (dataWithoutSchema.indexOf('bc1') === 0 && dataWithoutSchema.indexOf('?') === -1)) { if (
btcAddressRx.test(dataWithoutSchema) ||
((dataWithoutSchema.indexOf('bc1') === 0 || dataWithoutSchema.indexOf('BC1') === 0) && dataWithoutSchema.indexOf('?') === -1)
) {
recipients[[this.state.recipientsScrollIndex]].address = dataWithoutSchema; recipients[[this.state.recipientsScrollIndex]].address = dataWithoutSchema;
this.setState({ this.setState({
address: recipients, address: recipients,
@ -161,7 +167,7 @@ export default class SendDetails extends Component {
this.setState({ isLoading: false }); this.setState({ isLoading: false });
} }
console.log(options); console.log(options);
if (btcAddressRx.test(address) || address.indexOf('bc1') === 0) { if (btcAddressRx.test(address) || address.indexOf('bc1') === 0 || address.indexOf('BC1') === 0) {
recipients[[this.state.recipientsScrollIndex]].address = address; recipients[[this.state.recipientsScrollIndex]].address = address;
recipients[[this.state.recipientsScrollIndex]].amount = options.amount; recipients[[this.state.recipientsScrollIndex]].amount = options.amount;
this.setState({ this.setState({
@ -707,6 +713,49 @@ export default class SendDetails extends Component {
); );
}; };
importTransaction = async () => {
try {
const res = await DocumentPicker.pick({
type: Platform.OS === 'ios' ? ['io.bluewallet.psbt', 'io.bluewallet.psbt.txn'] : [DocumentPicker.types.allFiles],
});
if (DeeplinkSchemaMatch.isPossiblyPSBTFile(res.uri)) {
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.
// alternatively, user can export psbt file, sign it externally and then import it
this.props.navigation.navigate('PsbtWithHardwareWallet', {
memo: this.state.memo,
fromWallet: this.state.fromWallet,
psbt: file,
isFirstPSBTAlreadyBase64: true,
});
this.setState({ isLoading: false });
return;
}
} else {
throw new Error();
}
} else if (DeeplinkSchemaMatch.isTXNFile(res.uri)) {
const file = await RNFS.readFile(res.uri, 'ascii');
this.props.navigation.navigate('PsbtWithHardwareWallet', {
memo: this.state.memo,
fromWallet: this.state.fromWallet,
txhex: file,
});
this.setState({ isLoading: false, isAdvancedTransactionOptionsVisible: false });
return;
}
} catch (err) {
if (!DocumentPicker.isCancel(err)) {
alert('The selected file does not contain a signed transaction that can be imported.');
}
}
};
renderAdvancedTransactionOptionsModal = () => { renderAdvancedTransactionOptionsModal = () => {
const isSendMaxUsed = this.state.addresses.some(element => element.amount === BitcoinUnit.MAX); const isSendMaxUsed = this.state.addresses.some(element => element.amount === BitcoinUnit.MAX);
return ( return (
@ -738,6 +787,11 @@ export default class SendDetails extends Component {
onSwitch={this.onReplaceableFeeSwitchValueChanged} onSwitch={this.onReplaceableFeeSwitchValueChanged}
/> />
)} )}
{this.state.fromWallet.type === WatchOnlyWallet.type &&
this.state.fromWallet.isHd() &&
this.state.fromWallet.getSecret().startsWith('zpub') && (
<BlueListItem title="Import Transaction" hideChevron component={TouchableOpacity} onPress={this.importTransaction} />
)}
{this.state.fromWallet.allowBatchSend() && ( {this.state.fromWallet.allowBatchSend() && (
<> <>
<BlueListItem <BlueListItem

View file

@ -1,6 +1,18 @@
/* global alert */ /* global alert */
import React, { Component } from 'react'; 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,
PermissionsAndroid,
} from 'react-native';
import QRCode from 'react-native-qrcode-svg'; import QRCode from 'react-native-qrcode-svg';
import { Icon, Text } from 'react-native-elements'; import { Icon, Text } from 'react-native-elements';
import { import {
@ -13,8 +25,11 @@ import {
BlueCopyToClipboardButton, BlueCopyToClipboardButton,
} from '../../BlueComponents'; } from '../../BlueComponents';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Share from 'react-native-share';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import { RNCamera } from 'react-native-camera'; import { RNCamera } from 'react-native-camera';
import RNFS from 'react-native-fs';
import DocumentPicker from 'react-native-document-picker';
let loc = require('../../loc'); let loc = require('../../loc');
let EV = require('../../events'); let EV = require('../../events');
let BlueElectrum = require('../../BlueElectrum'); let BlueElectrum = require('../../BlueElectrum');
@ -33,10 +48,19 @@ export default class PsbtWithHardwareWallet extends Component {
onBarCodeRead = ret => { onBarCodeRead = ret => {
if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.pausePreview(); if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.pausePreview();
if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) {
// this looks like NOT base64, so maybe its transaction's hex
this.setState({ renderScanner: false, txhex: ret.data });
return;
}
this.setState({ renderScanner: false }, () => { this.setState({ renderScanner: false }, () => {
console.log(ret.data);
try { 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(),
ret.data,
);
this.setState({ txhex: Tx.toHex() }); this.setState({ txhex: Tx.toHex() });
} catch (Err) { } catch (Err) {
alert(Err); alert(Err);
@ -46,18 +70,47 @@ export default class PsbtWithHardwareWallet extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
isLoading: false, isLoading: false,
renderScanner: false, renderScanner: false,
qrCodeHeight: height > width ? width - 40 : width / 2, qrCodeHeight: height > width ? width - 40 : width / 3,
memo: props.navigation.getParam('memo'), memo: props.navigation.getParam('memo'),
psbt: props.navigation.getParam('psbt'), psbt: props.navigation.getParam('psbt'),
fromWallet: props.navigation.getParam('fromWallet'), fromWallet: props.navigation.getParam('fromWallet'),
isFirstPSBTAlreadyBase64: props.navigation.getParam('isFirstPSBTAlreadyBase64'),
isSecondPSBTAlreadyBase64: false,
deepLinkPSBT: undefined,
txhex: props.navigation.getParam('txhex') || undefined,
}; };
this.fileName = `${Date.now()}.psbt`;
} }
async componentDidMount() { static getDerivedStateFromProps(nextProps, prevState) {
const deepLinkPSBT = nextProps.navigation.state.params.deepLinkPSBT;
const txhex = nextProps.navigation.state.params.txhex;
if (deepLinkPSBT) {
try {
let Tx = prevState.fromWallet.combinePsbt(
prevState.isFirstPSBTAlreadyBase64 ? prevState.psbt : prevState.psbt.toBase64(),
deepLinkPSBT,
);
return {
...prevState,
txhex: Tx.toHex(),
};
} catch (Err) {
alert(Err);
}
} else if (txhex) {
return {
...prevState,
txhex: txhex,
};
}
return prevState;
}
componentDidMount() {
console.log('send/psbtWithHardwareWallet - componentDidMount'); console.log('send/psbtWithHardwareWallet - componentDidMount');
} }
@ -185,6 +238,56 @@ export default class PsbtWithHardwareWallet extends Component {
); );
} }
exportPSBT = async () => {
if (Platform.OS === 'ios') {
const filePath = RNFS.TemporaryDirectoryPath + `/${this.fileName}`;
await RNFS.writeFile(filePath, this.state.isFirstPSBTAlreadyBase64 ? this.state.psbt : this.state.psbt.toBase64());
Share.open({
url: 'file://' + filePath,
})
.catch(error => console.log(error))
.finally(() => {
RNFS.unlink(filePath);
});
} else if (Platform.OS === 'android') {
const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, {
title: 'BlueWallet Storage Access Permission',
message: 'BlueWallet needs your permission to access your storage to save this transaction.',
buttonNeutral: 'Ask Me Later',
buttonNegative: 'Cancel',
buttonPositive: 'OK',
});
if (granted === PermissionsAndroid.RESULTS.GRANTED) {
console.log('Storage Permission: Granted');
const filePath = RNFS.ExternalCachesDirectoryPath + `/${this.fileName}`;
await RNFS.writeFile(filePath, this.state.isFirstPSBTAlreadyBase64 ? this.state.psbt : this.state.psbt.toBase64());
alert(`This transaction has been saved in ${filePath}`);
} else {
console.log('Storage Permission: Denied');
}
}
};
openSignedTransaction = async () => {
try {
const res = await DocumentPicker.pick({
type: Platform.OS === 'ios' ? ['io.bluewallet.psbt', 'io.bluewallt.psbt.txn'] : [DocumentPicker.types.allFiles],
});
const file = await RNFS.readFile(res.uri);
if (file) {
this.setState({ isSecondPSBTAlreadyBase64: true }, () => this.onBarCodeRead({ data: file }));
} 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() { render() {
if (this.state.isLoading) { if (this.state.isLoading) {
return ( return (
@ -200,27 +303,58 @@ export default class PsbtWithHardwareWallet extends Component {
return ( return (
<SafeBlueArea style={{ flex: 1 }}> <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 }}> <View style={{ flexDirection: 'row', justifyContent: 'center', paddingTop: 16, paddingBottom: 16 }}>
<BlueCard> <BlueCard>
<BlueText>This is partially signed bitcoin transaction (PSBT). Please finish signing it with your hardware wallet.</BlueText> <BlueText>This is partially signed bitcoin transaction (PSBT). Please finish signing it with your hardware wallet.</BlueText>
<BlueSpacing20 /> <BlueSpacing20 />
<QRCode <QRCode
value={this.state.psbt.toBase64()} value={this.state.isFirstPSBTAlreadyBase64 ? this.state.psbt : this.state.psbt.toBase64()}
size={this.state.qrCodeHeight} size={this.state.qrCodeHeight}
color={BlueApp.settings.foregroundColor} color={BlueApp.settings.foregroundColor}
logoBackgroundColor={BlueApp.settings.brandingColor} logoBackgroundColor={BlueApp.settings.brandingColor}
ecl={'L'} ecl={'L'}
/> />
<BlueSpacing20 /> <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 to file'}
/>
<BlueSpacing20 /> <BlueSpacing20 />
<View style={{ justifyContent: 'center', alignItems: 'center' }}> <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> </View>
</BlueCard> </BlueCard>
</View> </View>
</View> </ScrollView>
</SafeBlueArea> </SafeBlueArea>
); );
} }

View file

@ -6,16 +6,20 @@ import { Icon } from 'react-native-elements';
import ImagePicker from 'react-native-image-picker'; import ImagePicker from 'react-native-image-picker';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useNavigationParam, useNavigation } from 'react-navigation-hooks'; 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'); const LocalQRCode = require('@remobile/react-native-qrcode-local-image');
const ScanQRCode = ({ const ScanQRCode = ({
onBarScanned = useNavigationParam('onBarScanned'), onBarScanned = useNavigationParam('onBarScanned'),
cameraPreviewIsPaused = false, cameraPreviewIsPaused = false,
showCloseButton = true, showCloseButton = true,
showFileImportButton = useNavigationParam('showFileImportButton') || false,
launchedBy = useNavigationParam('launchedBy'), launchedBy = useNavigationParam('launchedBy'),
}) => { }) => {
if (!launchedBy || !onBarScanned) console.warn('Necessary params missing');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { navigate } = useNavigation(); const { navigate, goBack } = useNavigation();
const onBarCodeRead = ret => { const onBarCodeRead = ret => {
if (!isLoading && !cameraPreviewIsPaused) { if (!isLoading && !cameraPreviewIsPaused) {
@ -24,7 +28,11 @@ const ScanQRCode = ({
if (showCloseButton && launchedBy) { if (showCloseButton && launchedBy) {
navigate(launchedBy); navigate(launchedBy);
} }
onBarScanned(ret.data); if (ret.additionalProperties) {
onBarScanned(ret.data, ret.additionalProperties);
} else {
onBarScanned(ret.data);
}
} catch (e) { } catch (e) {
console.log(e); console.log(e);
} }
@ -32,6 +40,30 @@ const ScanQRCode = ({
setIsLoading(false); setIsLoading(false);
}; };
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) {
let masterFingerprint;
if (fileParsed.keystore.ckcc_xfp) {
masterFingerprint = Number(fileParsed.keystore.ckcc_xfp);
}
onBarCodeRead({ data: fileParsed.keystore.xpub, additionalProperties: { masterFingerprint } });
} 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);
};
useEffect(() => {}, [cameraPreviewIsPaused]); useEffect(() => {}, [cameraPreviewIsPaused]);
return ( return (
@ -62,7 +94,7 @@ const ScanQRCode = ({
right: 16, right: 16,
top: 64, top: 64,
}} }}
onPress={() => navigate(launchedBy)} onPress={() => (launchedBy ? navigate(launchedBy) : goBack(null))}
> >
<Image style={{ alignSelf: 'center' }} source={require('../../img/close-white.png')} /> <Image style={{ alignSelf: 'center' }} source={require('../../img/close-white.png')} />
</TouchableOpacity> </TouchableOpacity>
@ -106,6 +138,23 @@ const ScanQRCode = ({
> >
<Icon name="image" type="font-awesome" color="#0c2550" /> <Icon name="image" type="font-awesome" color="#0c2550" />
</TouchableOpacity> </TouchableOpacity>
{showFileImportButton && (
<TouchableOpacity
style={{
width: 40,
height: 40,
backgroundColor: '#FFFFFF',
justifyContent: 'center',
borderRadius: 20,
position: 'absolute',
left: 96,
bottom: 48,
}}
onPress={showFilePicker}
>
<Icon name="file-import" type="material-community" color="#0c2550" />
</TouchableOpacity>
)}
</View> </View>
); );
}; };
@ -117,6 +166,7 @@ ScanQRCode.propTypes = {
launchedBy: PropTypes.string, launchedBy: PropTypes.string,
onBarScanned: PropTypes.func, onBarScanned: PropTypes.func,
cameraPreviewIsPaused: PropTypes.bool, cameraPreviewIsPaused: PropTypes.bool,
showFileImportButton: PropTypes.bool,
showCloseButton: PropTypes.bool, showCloseButton: PropTypes.bool,
}; };
export default ScanQRCode; export default ScanQRCode;

View file

@ -203,7 +203,7 @@ export default class SendCreate extends Component {
<SafeBlueArea style={{ flex: 1, paddingTop: 20 }}> <SafeBlueArea style={{ flex: 1, paddingTop: 20 }}>
<BlueSpacing /> <BlueSpacing />
<BlueCard title={'Replace Transaction'} style={{ alignItems: 'center', flex: 1 }}> <BlueCard title={'Replace Transaction'} style={{ alignItems: 'center', flex: 1 }}>
<BlueText>This is transaction hex, signed and ready to be broadcast to the network. Continue?</BlueText> <BlueText>This is your transaction's hex, signed and ready to be broadcasted to the network. Continue?</BlueText>
<TextInput <TextInput
style={{ style={{

View file

@ -6,6 +6,7 @@ import {
Text, Text,
TextInput, TextInput,
Alert, Alert,
KeyboardAvoidingView,
TouchableOpacity, TouchableOpacity,
Keyboard, Keyboard,
TouchableWithoutFeedback, TouchableWithoutFeedback,
@ -19,12 +20,13 @@ import { HDLegacyP2PKHWallet } from '../../class/hd-legacy-p2pkh-wallet';
import { HDSegwitP2SHWallet } from '../../class/hd-segwit-p2sh-wallet'; import { HDSegwitP2SHWallet } from '../../class/hd-segwit-p2sh-wallet';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import Biometric from '../../class/biometrics'; import Biometric from '../../class/biometrics';
import { HDSegwitBech32Wallet, WatchOnlyWallet } from '../../class'; import { HDSegwitBech32Wallet, SegwitP2SHWallet, LegacyWallet, SegwitBech32Wallet, WatchOnlyWallet } from '../../class';
let EV = require('../../events'); import { ScrollView } from 'react-native-gesture-handler';
let prompt = require('../../prompt'); const EV = require('../../events');
const prompt = require('../../prompt');
/** @type {AppStorage} */ /** @type {AppStorage} */
let BlueApp = require('../../BlueApp'); const BlueApp = require('../../BlueApp');
let loc = require('../../loc'); const loc = require('../../loc');
export default class WalletDetails extends Component { export default class WalletDetails extends Component {
static navigationOptions = ({ navigation }) => ({ static navigationOptions = ({ navigation }) => ({
@ -54,7 +56,7 @@ export default class WalletDetails extends Component {
isLoading, isLoading,
walletName: wallet.getLabel(), walletName: wallet.getLabel(),
wallet, wallet,
useWithHardwareWallet: !!wallet.use_with_hardware_wallet, useWithHardwareWallet: wallet.useWithHardwareWalletEnabled(),
}; };
this.props.navigation.setParams({ isLoading, saveAction: () => this.setLabel() }); this.props.navigation.setParams({ isLoading, saveAction: () => this.setLabel() });
} }
@ -70,7 +72,9 @@ export default class WalletDetails extends Component {
setLabel() { setLabel() {
this.props.navigation.setParams({ isLoading: true }); this.props.navigation.setParams({ isLoading: true });
this.setState({ isLoading: true }, async () => { this.setState({ isLoading: true }, async () => {
this.state.wallet.setLabel(this.state.walletName); if (this.state.walletName.trim().length > 0) {
this.state.wallet.setLabel(this.state.walletName);
}
BlueApp.saveToDisk(); BlueApp.saveToDisk();
alert('Wallet updated.'); alert('Wallet updated.');
this.props.navigation.goBack(null); this.props.navigation.goBack(null);
@ -106,7 +110,7 @@ export default class WalletDetails extends Component {
async onUseWithHardwareWalletSwitch(value) { async onUseWithHardwareWalletSwitch(value) {
this.setState((state, props) => { this.setState((state, props) => {
let wallet = state.wallet; let wallet = state.wallet;
wallet.use_with_hardware_wallet = !!value; wallet.setUseWithHardwareWalletEnabled(value);
return { useWithHardwareWallet: !!value, wallet }; return { useWithHardwareWallet: !!value, wallet };
}); });
} }
@ -122,170 +126,188 @@ export default class WalletDetails extends Component {
return ( return (
<SafeBlueArea style={{ flex: 1 }}> <SafeBlueArea style={{ flex: 1 }}>
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}> <TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
<View style={{ flex: 1 }}> <KeyboardAvoidingView behavior="position">
<BlueCard style={{ alignItems: 'center', flex: 1 }}> <ScrollView contentContainerStyle={{ flexGrow: 1 }}>
{(() => { <BlueCard style={{ alignItems: 'center', flex: 1 }}>
if (this.state.wallet.getAddress()) { {(() => {
return ( if (
<React.Fragment> [LegacyWallet.type, SegwitBech32Wallet.type, SegwitP2SHWallet.type].includes(this.state.wallet.type) ||
<Text style={{ color: '#0c2550', fontWeight: '500', fontSize: 14, marginVertical: 12 }}> (this.state.wallet.type === WatchOnlyWallet.type && !this.state.wallet.isHd())
{loc.wallets.details.address.toLowerCase()} ) {
</Text> return (
<Text style={{ color: '#81868e', fontWeight: '500', fontSize: 14 }}>{this.state.wallet.getAddress()}</Text> <React.Fragment>
</React.Fragment> <Text style={{ color: '#0c2550', fontWeight: '500', fontSize: 14, marginVertical: 12 }}>
); {loc.wallets.details.address.toLowerCase()}
} </Text>
})()} <Text style={{ color: '#81868e', fontWeight: '500', fontSize: 14 }}>{this.state.wallet.getAddress()}</Text>
<Text style={{ color: '#0c2550', fontWeight: '500', fontSize: 14, marginVertical: 16 }}> </React.Fragment>
{loc.wallets.add.wallet_name.toLowerCase()} );
</Text>
<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={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 });
}}
numberOfLines={1}
style={{ flex: 1, marginHorizontal: 8, minHeight: 33 }}
editable={!this.state.isLoading}
underlineColorAndroid="transparent"
/>
</View>
<Text style={{ color: '#0c2550', fontWeight: '500', fontSize: 14, marginVertical: 12 }}>
{loc.wallets.details.type.toLowerCase()}
</Text>
<Text style={{ color: '#81868e', fontWeight: '500', fontSize: 14 }}>{this.state.wallet.typeReadable}</Text>
{this.state.wallet.type === LightningCustodianWallet.type && (
<React.Fragment>
<Text style={{ color: '#0c2550', fontWeight: '500', fontSize: 14, marginVertical: 12 }}>{'connected to'}</Text>
<BlueText>{this.state.wallet.getBaseURI()}</BlueText>
</React.Fragment>
)}
<View>
<BlueSpacing20 />
{this.state.wallet.type === WatchOnlyWallet.type && this.state.wallet.getSecret().startsWith('zpub') && (
<React.Fragment>
<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
onPress={() =>
this.props.navigation.navigate('WalletExport', {
address: this.state.wallet.getAddress(),
secret: this.state.wallet.getSecret(),
})
} }
title={loc.wallets.details.export_backup} })()}
/> <Text style={{ color: '#0c2550', fontWeight: '500', fontSize: 14, marginVertical: 16 }}>
{loc.wallets.add.wallet_name.toLowerCase()}
</Text>
<BlueSpacing20 /> <View
style={{
{(this.state.wallet.type === HDLegacyBreadwalletWallet.type || flexDirection: 'row',
this.state.wallet.type === HDLegacyP2PKHWallet.type || borderColor: '#d2d2d2',
this.state.wallet.type === HDSegwitBech32Wallet.type || borderBottomColor: '#d2d2d2',
this.state.wallet.type === HDSegwitP2SHWallet.type) && ( borderWidth: 1.0,
<React.Fragment> borderBottomWidth: 0.5,
<BlueButton backgroundColor: '#f5f5f5',
onPress={() => minHeight: 44,
this.props.navigation.navigate('WalletXpub', { height: 44,
secret: this.state.wallet.getSecret(), alignItems: 'center',
}) borderRadius: 4,
}}
>
<TextInput
placeholder={loc.send.details.note_placeholder}
value={this.state.walletName}
onChangeText={text => {
this.setState({ walletName: text });
}}
onBlur={() => {
if (this.state.walletName.trim().length === 0) {
const walletLabel = this.state.wallet.getLabel();
this.setState({ walletName: walletLabel });
} }
title={loc.wallets.details.show_xpub} }}
/> numberOfLines={1}
style={{ flex: 1, marginHorizontal: 8, minHeight: 33 }}
<BlueSpacing20 /> editable={!this.state.isLoading}
underlineColorAndroid="transparent"
/>
</View>
<BlueSpacing20 />
<Text style={{ color: '#0c2550', fontWeight: '500', fontSize: 14, marginVertical: 12 }}>
{loc.wallets.details.type.toLowerCase()}
</Text>
<Text style={{ color: '#81868e', fontWeight: '500', fontSize: 14 }}>{this.state.wallet.typeReadable}</Text>
{this.state.wallet.type === LightningCustodianWallet.type && (
<React.Fragment>
<Text style={{ color: '#0c2550', fontWeight: '500', fontSize: 14, marginVertical: 12 }}>{'connected to'}</Text>
<BlueText>{this.state.wallet.getBaseURI()}</BlueText>
</React.Fragment> </React.Fragment>
)} )}
<View>
<BlueSpacing20 />
{this.state.wallet.type === WatchOnlyWallet.type && this.state.wallet.getSecret().startsWith('zpub') && (
<>
<Text style={{ color: '#0c2550', fontWeight: '500', fontSize: 14, marginVertical: 16 }}>{'advanced'}</Text>
<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>
<React.Fragment>
<Text style={{ color: '#0c2550', fontWeight: '500', fontSize: 14, marginVertical: 12 }}>
{loc.wallets.details.master_fingerprint.toLowerCase()}
</Text>
<Text style={{ color: '#81868e', fontWeight: '500', fontSize: 14 }}>
{this.state.wallet.getMasterFingerprintHex()}
</Text>
</React.Fragment>
<BlueSpacing20 />
</>
)}
{this.state.wallet.type !== LightningCustodianWallet.type && (
<BlueButton <BlueButton
icon={{
name: 'shopping-cart',
type: 'font-awesome',
color: BlueApp.settings.buttonTextColor,
}}
onPress={() => onPress={() =>
this.props.navigation.navigate('BuyBitcoin', { this.props.navigation.navigate('WalletExport', {
address: this.state.wallet.getAddress(), address: this.state.wallet.getAddress(),
secret: this.state.wallet.getSecret(), secret: this.state.wallet.getSecret(),
}) })
} }
title={loc.wallets.details.buy_bitcoin} title={loc.wallets.details.export_backup}
/> />
)}
<BlueSpacing20 />
<TouchableOpacity <BlueSpacing20 />
style={{ alignItems: 'center' }}
onPress={() => {
ReactNativeHapticFeedback.trigger('notificationWarning', { ignoreAndroidSystemSettings: false });
Alert.alert(
loc.wallets.details.delete + ' ' + loc.wallets.details.title,
loc.wallets.details.are_you_sure,
[
{
text: loc.wallets.details.yes_delete,
onPress: async () => {
const isBiometricsEnabled = await Biometric.isBiometricUseCapableAndEnabled();
if (isBiometricsEnabled) { {(this.state.wallet.type === HDLegacyBreadwalletWallet.type ||
if (!(await Biometric.unlockWithBiometrics())) { this.state.wallet.type === HDLegacyP2PKHWallet.type ||
return; this.state.wallet.type === HDSegwitBech32Wallet.type ||
this.state.wallet.type === HDSegwitP2SHWallet.type) && (
<React.Fragment>
<BlueButton
onPress={() =>
this.props.navigation.navigate('WalletXpub', {
secret: this.state.wallet.getSecret(),
})
}
title={loc.wallets.details.show_xpub}
/>
<BlueSpacing20 />
</React.Fragment>
)}
{this.state.wallet.type !== LightningCustodianWallet.type && (
<BlueButton
icon={{
name: 'shopping-cart',
type: 'font-awesome',
color: BlueApp.settings.buttonTextColor,
}}
onPress={() =>
this.props.navigation.navigate('BuyBitcoin', {
address: this.state.wallet.getAddress(),
secret: this.state.wallet.getSecret(),
})
}
title={loc.wallets.details.buy_bitcoin}
/>
)}
<BlueSpacing20 />
<TouchableOpacity
style={{ alignItems: 'center' }}
onPress={() => {
ReactNativeHapticFeedback.trigger('notificationWarning', { ignoreAndroidSystemSettings: false });
Alert.alert(
loc.wallets.details.delete + ' ' + loc.wallets.details.title,
loc.wallets.details.are_you_sure,
[
{
text: loc.wallets.details.yes_delete,
onPress: async () => {
const isBiometricsEnabled = await Biometric.isBiometricUseCapableAndEnabled();
if (isBiometricsEnabled) {
if (!(await Biometric.unlockWithBiometrics())) {
return;
}
} }
} if (this.state.wallet.getBalance() > 0 && this.state.wallet.allowSend()) {
if (this.state.wallet.getBalance() > 0) { this.presentWalletHasBalanceAlert();
this.presentWalletHasBalanceAlert(); } else {
} else { this.props.navigation.setParams({ isLoading: true });
this.props.navigation.setParams({ isLoading: true }); this.setState({ isLoading: true }, async () => {
this.setState({ isLoading: true }, async () => { BlueApp.deleteWallet(this.state.wallet);
BlueApp.deleteWallet(this.state.wallet); ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false });
ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false }); await BlueApp.saveToDisk();
await BlueApp.saveToDisk(); EV(EV.enum.TRANSACTIONS_COUNT_CHANGED);
EV(EV.enum.TRANSACTIONS_COUNT_CHANGED); EV(EV.enum.WALLETS_COUNT_CHANGED);
EV(EV.enum.WALLETS_COUNT_CHANGED); this.props.navigation.navigate('Wallets');
this.props.navigation.navigate('Wallets'); });
}); }
} },
}, },
style: 'destructive', { text: loc.wallets.details.no_cancel, onPress: () => {}, style: 'cancel' },
}, ],
{ text: loc.wallets.details.no_cancel, onPress: () => {}, style: 'cancel' }, { cancelable: false },
], );
{ cancelable: false }, }}
); >
}} <Text style={{ color: '#d0021b', fontSize: 15, fontWeight: '500' }}>{loc.wallets.details.delete}</Text>
> </TouchableOpacity>
<Text style={{ color: '#d0021b', fontSize: 15, fontWeight: '500' }}>{loc.wallets.details.delete}</Text> </View>
</TouchableOpacity> </BlueCard>
</View> </ScrollView>
</BlueCard> </KeyboardAvoidingView>
</View>
</TouchableWithoutFeedback> </TouchableWithoutFeedback>
</SafeBlueArea> </SafeBlueArea>
); );

View file

@ -35,9 +35,14 @@ const WalletsImport = () => {
importMnemonic(importText); importMnemonic(importText);
}; };
const importMnemonic = importText => { /**
*
* @param importText
* @param additionalProperties key-values passed from outside. Used only to set up `masterFingerprint` property for watch-only wallet
*/
const importMnemonic = (importText, additionalProperties) => {
try { try {
WalletImport.processImportText(importText); WalletImport.processImportText(importText, additionalProperties);
dismiss(); dismiss();
} catch (error) { } catch (error) {
alert(loc.wallets.import.error); alert(loc.wallets.import.error);
@ -45,9 +50,14 @@ const WalletsImport = () => {
} }
}; };
const onBarScanned = value => { /**
*
* @param value
* @param additionalProperties key-values passed from outside. Used only to set up `masterFingerprint` property for watch-only wallet
*/
const onBarScanned = (value, additionalProperties) => {
setImportText(value); setImportText(value);
importMnemonic(value); importMnemonic(value, additionalProperties);
}; };
return ( return (
@ -110,7 +120,7 @@ const WalletsImport = () => {
<BlueButtonLink <BlueButtonLink
title={loc.wallets.import.scan_qr} title={loc.wallets.import.scan_qr}
onPress={() => { onPress={() => {
navigate('ScanQrAddress', { onBarScanned }); navigate('ScanQrAddress', { launchedBy: 'ImportWallet', onBarScanned, showFileImportButton: true });
}} }}
/> />
</View> </View>

View file

@ -1,4 +1,3 @@
/* global alert */
import React, { Component } from 'react'; import React, { Component } from 'react';
import { import {
View, View,
@ -68,7 +67,7 @@ export default class WalletsList extends Component {
console.log('fetch all wallet txs took', (end - start) / 1000, 'sec'); console.log('fetch all wallet txs took', (end - start) / 1000, 'sec');
} catch (error) { } catch (error) {
noErr = false; noErr = false;
alert(error); console.log(error);
} }
if (noErr) this.redrawScreen(); if (noErr) this.redrawScreen();
}); });
@ -111,7 +110,6 @@ export default class WalletsList extends Component {
console.log('fetch tx took', (end - start) / 1000, 'sec'); console.log('fetch tx took', (end - start) / 1000, 'sec');
} catch (err) { } catch (err) {
noErr = false; noErr = false;
alert(err);
console.warn(err); console.warn(err);
} }
if (noErr) await BlueApp.saveToDisk(); // caching if (noErr) await BlueApp.saveToDisk(); // caching
@ -262,7 +260,6 @@ export default class WalletsList extends Component {
} }
} catch (Err) { } catch (Err) {
noErr = false; noErr = false;
alert(Err);
console.warn(Err); console.warn(Err);
} }
@ -338,16 +335,7 @@ export default class WalletsList extends Component {
}} }}
onWillBlur={() => this.setState({ cameraPreviewIsPaused: true })} onWillBlur={() => this.setState({ cameraPreviewIsPaused: true })}
/> />
<ScrollView <ScrollView contentContainerStyle={{ flex: 1 }}>
contentContainerStyle={{ flex: 1 }}
refreshControl={
<RefreshControl
onRefresh={() => this.refreshTransactions()}
refreshing={!this.state.isFlatListRefreshControlHidden}
shouldRefresh={this.state.timeElpased}
/>
}
>
<Swiper <Swiper
style={styles.wrapper} style={styles.wrapper}
onIndexChanged={this.onSwiperIndexChanged} onIndexChanged={this.onSwiperIndexChanged}

View file

@ -7,13 +7,12 @@ import { LightningCustodianWallet } from '../../class/lightning-custodian-wallet
import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import WalletGradient from '../../class/walletGradient'; import WalletGradient from '../../class/walletGradient';
import { useNavigationParam } from 'react-navigation-hooks'; import { useNavigationParam } from 'react-navigation-hooks';
import { Chain } from '../../models/bitcoinUnits';
/** @type {AppStorage} */ /** @type {AppStorage} */
const BlueApp = require('../../BlueApp'); const BlueApp = require('../../BlueApp');
const loc = require('../../loc'); const loc = require('../../loc');
const SelectWallet = () => { const SelectWallet = () => {
const chainType = useNavigationParam('chainType') || Chain.ONCHAIN; const chainType = useNavigationParam('chainType');
const onWalletSelect = useNavigationParam('onWalletSelect'); const onWalletSelect = useNavigationParam('onWalletSelect');
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const data = chainType const data = chainType

View file

@ -16,6 +16,7 @@ import {
StatusBar, StatusBar,
Linking, Linking,
KeyboardAvoidingView, KeyboardAvoidingView,
Alert,
} from 'react-native'; } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { NavigationEvents } from 'react-navigation'; import { NavigationEvents } from 'react-navigation';
@ -29,7 +30,7 @@ import {
} from '../../BlueComponents'; } from '../../BlueComponents';
import WalletGradient from '../../class/walletGradient'; import WalletGradient from '../../class/walletGradient';
import { Icon } from 'react-native-elements'; import { Icon } from 'react-native-elements';
import { LightningCustodianWallet } from '../../class'; import { LightningCustodianWallet, WatchOnlyWallet } from '../../class';
import Handoff from 'react-native-handoff'; import Handoff from 'react-native-handoff';
import Modal from 'react-native-modal'; import Modal from 'react-native-modal';
import NavigationService from '../../NavigationService'; import NavigationService from '../../NavigationService';
@ -400,7 +401,7 @@ export default class WalletTransactions extends Component {
} }
}; };
async onWillBlur() { onWillBlur() {
StatusBar.setBarStyle('dark-content'); StatusBar.setBarStyle('dark-content');
} }
@ -409,6 +410,14 @@ export default class WalletTransactions extends Component {
clearInterval(this.interval); 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 => { renderItem = item => {
return ( return (
<BlueTransactionListItem <BlueTransactionListItem
@ -569,18 +578,51 @@ export default class WalletTransactions extends Component {
})()} })()}
{(() => { {(() => {
if (this.state.wallet.allowSend()) { if (
this.state.wallet.allowSend() ||
(this.state.wallet.type === WatchOnlyWallet.type &&
this.state.wallet.isHd() &&
this.state.wallet.getSecret().startsWith('zpub'))
) {
return ( return (
<BlueSendButtonIcon <BlueSendButtonIcon
onPress={() => { onPress={() => {
if (this.state.wallet.chain === Chain.OFFCHAIN) { if (this.state.wallet.chain === Chain.OFFCHAIN) {
navigate('ScanLndInvoice', { fromSecret: this.state.wallet.getSecret() }); navigate('ScanLndInvoice', { fromSecret: this.state.wallet.getSecret() });
} else { } else {
navigate('SendDetails', { if (
fromAddress: this.state.wallet.getAddress(), this.state.wallet.type === WatchOnlyWallet.type &&
fromSecret: this.state.wallet.getSecret(), this.state.wallet.isHd() &&
fromWallet: this.state.wallet, this.state.wallet.getSecret().startsWith('zpub')
}); ) {
if (this.state.wallet.useWithHardwareWalletEnabled()) {
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: () => {
const wallet = this.state.wallet;
wallet.setUseWithHardwareWalletEnabled(true);
this.setState({ wallet }, async () => {
await BlueApp.saveToDisk();
this.navigateToSendScreen();
});
},
style: 'default',
},
{ text: loc.send.details.cancel, onPress: () => {}, style: 'cancel' },
],
{ cancelable: false },
);
}
} else {
this.navigateToSendScreen();
}
} }
}} }}
/> />

View file

@ -7,7 +7,7 @@ describe('LightningCustodianWallet', () => {
let l1 = new LightningCustodianWallet(); let l1 = new LightningCustodianWallet();
it.skip('issue credentials', async () => { it.skip('issue credentials', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000; jasmine.DEFAULT_TIMEOUT_INTERVAL = 200 * 1000;
assert.ok(l1.refill_addressess.length === 0); assert.ok(l1.refill_addressess.length === 0);
assert.ok(l1._refresh_token_created_ts === 0); assert.ok(l1._refresh_token_created_ts === 0);
assert.ok(l1._access_token_created_ts === 0); assert.ok(l1._access_token_created_ts === 0);
@ -24,7 +24,7 @@ describe('LightningCustodianWallet', () => {
}); });
it('can create, auth and getbtc', async () => { it('can create, auth and getbtc', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000; jasmine.DEFAULT_TIMEOUT_INTERVAL = 200 * 1000;
assert.ok(l1.refill_addressess.length === 0); assert.ok(l1.refill_addressess.length === 0);
assert.ok(l1._refresh_token_created_ts === 0); assert.ok(l1._refresh_token_created_ts === 0);
assert.ok(l1._access_token_created_ts === 0); assert.ok(l1._access_token_created_ts === 0);
@ -51,7 +51,7 @@ describe('LightningCustodianWallet', () => {
}); });
it('can refresh token', async () => { it('can refresh token', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000; jasmine.DEFAULT_TIMEOUT_INTERVAL = 200 * 1000;
let oldRefreshToken = l1.refresh_token; let oldRefreshToken = l1.refresh_token;
let oldAccessToken = l1.access_token; let oldAccessToken = l1.access_token;
await l1.refreshAcessToken(); await l1.refreshAcessToken();
@ -62,7 +62,7 @@ describe('LightningCustodianWallet', () => {
}); });
it('can use existing login/pass', async () => { it('can use existing login/pass', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000; jasmine.DEFAULT_TIMEOUT_INTERVAL = 200 * 1000;
if (!process.env.BLITZHUB) { if (!process.env.BLITZHUB) {
console.error('process.env.BLITZHUB not set, skipped'); console.error('process.env.BLITZHUB not set, skipped');
return; return;
@ -100,11 +100,12 @@ describe('LightningCustodianWallet', () => {
let invoice = let invoice =
'lnbc1u1pdcqpt3pp5ltuevvq2g69kdrzcegrs9gfqjer45rwjc0w736qjl92yvwtxhn6qdp8dp6kuerjv4j9xct5daeks6tnyp3xc6t50f582cscqp2zrkghzl535xjav52ns0rpskcn20takzdr2e02wn4xqretlgdemg596acq5qtfqhjk4jpr7jk8qfuuka2k0lfwjsk9mchwhxcgxzj3tsp09gfpy'; 'lnbc1u1pdcqpt3pp5ltuevvq2g69kdrzcegrs9gfqjer45rwjc0w736qjl92yvwtxhn6qdp8dp6kuerjv4j9xct5daeks6tnyp3xc6t50f582cscqp2zrkghzl535xjav52ns0rpskcn20takzdr2e02wn4xqretlgdemg596acq5qtfqhjk4jpr7jk8qfuuka2k0lfwjsk9mchwhxcgxzj3tsp09gfpy';
let decoded = await l2.decodeInvoice(invoice); let decoded = l2.decodeInvoice(invoice);
assert.ok(decoded.payment_hash); assert.ok(decoded.payment_hash);
assert.ok(decoded.description); assert.ok(decoded.description);
assert.ok(decoded.num_satoshis); assert.ok(decoded.num_satoshis);
assert.strictEqual(parseInt(decoded.num_satoshis) * 1000, parseInt(decoded.num_millisatoshis));
await l2.checkRouteInvoice(invoice); await l2.checkRouteInvoice(invoice);
@ -112,15 +113,44 @@ describe('LightningCustodianWallet', () => {
invoice = 'gsom'; invoice = 'gsom';
let error = false; let error = false;
try { try {
await l2.decodeInvoice(invoice); l2.decodeInvoice(invoice);
} catch (Err) { } catch (Err) {
error = true; error = true;
} }
assert.ok(error); assert.ok(error);
}); });
it('decode can handle zero sats but present msats', async () => {
let l = new LightningCustodianWallet();
let decoded = l.decodeInvoice(
'lnbc89n1p0zptvhpp5j3h5e80vdlzn32df8y80nl2t7hssn74lzdr96ve0u4kpaupflx2sdphgfkx7cmtwd68yetpd5s9xct5v4kxc6t5v5s9gunpdeek66tnwd5k7mscqp2sp57m89zv0lrgc9zzaxy5p3d5rr2cap2pm6zm4n0ew9vyp2d5zf2mfqrzjqfxj8p6qjf5l8du7yuytkwdcjhylfd4gxgs48t65awjg04ye80mq7z990yqq9jsqqqqqqqqqqqqq05qqrc9qy9qsq9mynpa9ucxg53hwnvw323r55xdd3l6lcadzs584zvm4wdw5pv3eksdlcek425pxaqrn9u5gpw0dtpyl9jw2pynjtqexxgh50akwszjgq4ht4dh',
);
assert.strictEqual(decoded.num_satoshis, '8.9');
});
it('can decode invoice locally & remotely', async () => {
if (!process.env.BLITZHUB) {
console.error('process.env.BLITZHUB not set, skipped');
return;
}
let l2 = new LightningCustodianWallet();
l2.setSecret(process.env.BLITZHUB);
await l2.authorize();
let invoice =
'lnbc1u1pdcqpt3pp5ltuevvq2g69kdrzcegrs9gfqjer45rwjc0w736qjl92yvwtxhn6qdp8dp6kuerjv4j9xct5daeks6tnyp3xc6t50f582cscqp2zrkghzl535xjav52ns0rpskcn20takzdr2e02wn4xqretlgdemg596acq5qtfqhjk4jpr7jk8qfuuka2k0lfwjsk9mchwhxcgxzj3tsp09gfpy';
let decodedLocally = l2.decodeInvoice(invoice);
let decodedRemotely = await l2.decodeInvoiceRemote(invoice);
assert.strictEqual(decodedLocally.destination, decodedRemotely.destination);
assert.strictEqual(decodedLocally.num_satoshis, decodedRemotely.num_satoshis);
assert.strictEqual(decodedLocally.timestamp, decodedRemotely.timestamp);
assert.strictEqual(decodedLocally.expiry, decodedRemotely.expiry);
assert.strictEqual(decodedLocally.payment_hash, decodedRemotely.payment_hash);
assert.strictEqual(decodedLocally.description, decodedRemotely.description);
assert.strictEqual(decodedLocally.cltv_expiry, decodedRemotely.cltv_expiry);
});
it('can pay invoice', async () => { it('can pay invoice', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000; jasmine.DEFAULT_TIMEOUT_INTERVAL = 200 * 1000;
if (!process.env.BLITZHUB) { if (!process.env.BLITZHUB) {
console.error('process.env.BLITZHUB not set, skipped'); console.error('process.env.BLITZHUB not set, skipped');
return; return;
@ -155,7 +185,7 @@ describe('LightningCustodianWallet', () => {
await l2.fetchTransactions(); await l2.fetchTransactions();
let txLen = l2.transactions_raw.length; let txLen = l2.transactions_raw.length;
let decoded = await l2.decodeInvoice(invoice); let decoded = l2.decodeInvoice(invoice);
assert.ok(decoded.payment_hash); assert.ok(decoded.payment_hash);
assert.ok(decoded.description); assert.ok(decoded.description);
@ -194,7 +224,7 @@ describe('LightningCustodianWallet', () => {
}); });
it('can create invoice and pay other blitzhub invoice', async () => { it('can create invoice and pay other blitzhub invoice', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000; jasmine.DEFAULT_TIMEOUT_INTERVAL = 200 * 1000;
if (!process.env.BLITZHUB) { if (!process.env.BLITZHUB) {
console.error('process.env.BLITZHUB not set, skipped'); console.error('process.env.BLITZHUB not set, skipped');
return; return;
@ -294,7 +324,7 @@ describe('LightningCustodianWallet', () => {
}); });
it('can pay free amount (tip) invoice', async function() { it('can pay free amount (tip) invoice', async function() {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000; jasmine.DEFAULT_TIMEOUT_INTERVAL = 200 * 1000;
if (!process.env.BLITZHUB) { if (!process.env.BLITZHUB) {
console.error('process.env.BLITZHUB not set, skipped'); console.error('process.env.BLITZHUB not set, skipped');
return; return;
@ -336,7 +366,7 @@ describe('LightningCustodianWallet', () => {
let oldBalance = +l2.balance; let oldBalance = +l2.balance;
let txLen = l2.transactions_raw.length; let txLen = l2.transactions_raw.length;
let decoded = await l2.decodeInvoice(invoice); let decoded = l2.decodeInvoice(invoice);
assert.ok(decoded.payment_hash); assert.ok(decoded.payment_hash);
assert.ok(decoded.description); assert.ok(decoded.description);
assert.strictEqual(+decoded.num_satoshis, 0); assert.strictEqual(+decoded.num_satoshis, 0);
@ -371,7 +401,7 @@ describe('LightningCustodianWallet', () => {
it('cant create zemo amt invoices yet', async () => { it('cant create zemo amt invoices yet', async () => {
let l1 = new LightningCustodianWallet(); let l1 = new LightningCustodianWallet();
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000; jasmine.DEFAULT_TIMEOUT_INTERVAL = 200 * 1000;
assert.ok(l1.refill_addressess.length === 0); assert.ok(l1.refill_addressess.length === 0);
assert.ok(l1._refresh_token_created_ts === 0); assert.ok(l1._refresh_token_created_ts === 0);
assert.ok(l1._access_token_created_ts === 0); assert.ok(l1._access_token_created_ts === 0);
@ -405,7 +435,7 @@ describe('LightningCustodianWallet', () => {
}); });
it('cant pay negative free amount', async () => { it('cant pay negative free amount', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000; jasmine.DEFAULT_TIMEOUT_INTERVAL = 200 * 1000;
if (!process.env.BLITZHUB) { if (!process.env.BLITZHUB) {
console.error('process.env.BLITZHUB not set, skipped'); console.error('process.env.BLITZHUB not set, skipped');
return; return;
@ -443,7 +473,7 @@ describe('LightningCustodianWallet', () => {
let oldBalance = +l2.balance; let oldBalance = +l2.balance;
let txLen = l2.transactions_raw.length; let txLen = l2.transactions_raw.length;
let decoded = await l2.decodeInvoice(invoice); let decoded = l2.decodeInvoice(invoice);
assert.ok(decoded.payment_hash); assert.ok(decoded.payment_hash);
assert.ok(decoded.description); assert.ok(decoded.description);
assert.strictEqual(+decoded.num_satoshis, 0); assert.strictEqual(+decoded.num_satoshis, 0);

View file

@ -1,24 +1,28 @@
/* global it, describe */ /* global it, describe */
let assert = require('assert'); let assert = require('assert');
const fs = require('fs');
describe('Localization', () => { describe('Localization', () => {
it('has all keys in all locales', async () => { it('has all keys in all locales', async () => {
let en = require('../../loc/en'); let en = require('../../loc/en');
let noErrors = true; let issues = 0;
for (let key1 of Object.keys(en)) { for (let key1 of Object.keys(en)) {
for (let key2 of Object.keys(en[key1])) { for (let key2 of Object.keys(en[key1])) {
// iterating all keys and subkeys in EN locale, which is main // iterating all keys and subkeys in EN locale, which is main
let files = fs.readdirSync('./loc/');
for (let lang of files) {
if (lang === 'en.js') continue; // iteratin all locales except EN
if (lang === 'index.js') continue;
for (let lang of ['es', 'pt_BR', 'pt_PT', 'ru', 'ua']) {
// iteratin all locales except EN
let locale = require('../../loc/' + lang); let locale = require('../../loc/' + lang);
if (typeof locale[key1] === 'undefined') { if (typeof locale[key1] === 'undefined') {
console.error('Missing: ' + lang + '.' + key1); console.error('Missing: ' + lang + '.' + key1);
noErrors = false; issues++;
} else if (typeof locale[key1][key2] === 'undefined') { } else if (typeof locale[key1][key2] === 'undefined') {
console.error('Missing: ' + lang + '.' + key1 + '.' + key2); console.error('Missing: ' + lang + '.' + key1 + '.' + key2);
noErrors = false; issues++;
} }
// level 1 & 2 done, doing level 3 (if it exists): // level 1 & 2 done, doing level 3 (if it exists):
@ -27,13 +31,13 @@ describe('Localization', () => {
for (let key3 of Object.keys(en[key1][key2])) { for (let key3 of Object.keys(en[key1][key2])) {
if (typeof locale[key1][key2][key3] === 'undefined') { if (typeof locale[key1][key2][key3] === 'undefined') {
console.error('Missing: ' + lang + '.' + key1 + '.' + key2 + '.' + key3); console.error('Missing: ' + lang + '.' + key1 + '.' + key2 + '.' + key3);
noErrors = false; issues++;
} }
} }
} }
} }
} }
} }
assert.ok(noErrors, 'Some localizations are missing keys'); assert.ok(issues === 0, 'Some localizations are missing keys. Total issues: ' + issues);
}); });
}); });

View file

@ -56,8 +56,10 @@ describe('Watch only wallet', () => {
let w = new WatchOnlyWallet(); let w = new WatchOnlyWallet();
w.setSecret('12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG'); w.setSecret('12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG');
assert.ok(w.valid()); assert.ok(w.valid());
assert.strictEqual(w.isHd(), false);
w.setSecret('3BDsBDxDimYgNZzsqszNZobqQq3yeUoJf2'); w.setSecret('3BDsBDxDimYgNZzsqszNZobqQq3yeUoJf2');
assert.ok(w.valid()); assert.ok(w.valid());
assert.strictEqual(w.isHd(), false);
w.setSecret('not valid'); w.setSecret('not valid');
assert.ok(!w.valid()); assert.ok(!w.valid());
@ -67,6 +69,9 @@ describe('Watch only wallet', () => {
assert.ok(w.valid()); assert.ok(w.valid());
w.setSecret('zpub6r7jhKKm7BAVx3b3nSnuadY1WnshZYkhK8gKFoRLwK9rF3Mzv28BrGcCGA3ugGtawi1WLb2vyjQAX9ZTDGU5gNk2bLdTc3iEXr6tzR1ipNP'); w.setSecret('zpub6r7jhKKm7BAVx3b3nSnuadY1WnshZYkhK8gKFoRLwK9rF3Mzv28BrGcCGA3ugGtawi1WLb2vyjQAX9ZTDGU5gNk2bLdTc3iEXr6tzR1ipNP');
assert.ok(w.valid()); assert.ok(w.valid());
assert.strictEqual(w.isHd(), true);
assert.strictEqual(w.getMasterFingerprint(), false);
assert.strictEqual(w.getMasterFingerprintHex(), '00000000');
}); });
it('can fetch balance & transactions from zpub HD', async () => { it('can fetch balance & transactions from zpub HD', async () => {
@ -111,6 +116,47 @@ describe('Watch only wallet', () => {
); );
}); });
it('can import coldcard/electrum compatible JSON skeleton wallet, and create a tx with master fingerprint', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 120 * 1000;
const skeleton =
'{"keystore": {"ckcc_xpub": "xpub661MyMwAqRbcGmUDQVKxmhEESB5xTk8hbsdTSV3Pmhm3HE9Fj3s45R9Y8LwyaQWjXXPytZjuhTKSyCBPeNrB1VVWQq1HCvjbEZ27k44oNmg", "xpub": "zpub6rFDtF1nuXZ9PUL4XzKURh3vJBW6Kj6TUrYL4qPtFNtDXtcTVfiqjQDyrZNwjwzt5HS14qdqo3Co2282Lv3Re6Y5wFZxAVuMEpeygnnDwfx", "label": "Coldcard Import 168DD603", "ckcc_xfp": 64392470, "type": "hardware", "hw_type": "coldcard", "derivation": "m/84\'/0\'/0\'"}, "wallet_type": "standard", "use_encryption": false, "seed_version": 17}';
let w = new WatchOnlyWallet();
w.setSecret(skeleton);
w.init();
assert.ok(w.valid());
assert.strictEqual(
w.getSecret(),
'zpub6rFDtF1nuXZ9PUL4XzKURh3vJBW6Kj6TUrYL4qPtFNtDXtcTVfiqjQDyrZNwjwzt5HS14qdqo3Co2282Lv3Re6Y5wFZxAVuMEpeygnnDwfx',
);
assert.strictEqual(w.getMasterFingerprint(), 64392470);
assert.strictEqual(w.getMasterFingerprintHex(), '168dd603');
const utxos = [
{
height: 618811,
value: 66600,
address: 'bc1qzqjwye4musmz56cg44ttnchj49zueh9yr0qsxt',
txId: '5df595dc09ee7a5c245b34ea519288137ffee731629c4ff322a6de4f72c06222',
vout: 0,
txid: '5df595dc09ee7a5c245b34ea519288137ffee731629c4ff322a6de4f72c06222',
amount: 66600,
wif: false,
confirmations: 1,
},
];
let { psbt } = await w.createTransaction(
utxos,
[{ address: 'bc1qdamevhw3zwm0ajsmyh39x8ygf0jr0syadmzepn', value: 5000 }],
22,
'bc1qtutssamysdkgd87df0afjct0mztx56qpze7wqe',
);
assert.strictEqual(
psbt.toBase64(),
'cHNidP8BAHECAAAAASJiwHJP3qYi80+cYjHn/n8TiJJR6jRbJFx67gnclfVdAAAAAAAAAACAAogTAAAAAAAAFgAUb3eWXdETtv7KGyXiUxyIS+Q3wJ1K3QAAAAAAABYAFF8XCHdkg2yGn81L+plhb9iWamgBAAAAAAABAR8oBAEAAAAAABYAFBAk4ma75DYqawitVrni8qlFzNykIgYDNK9TxoCjQ8P0+qI2Hu4hrnXnJuYAC3h2puZbgRORp+sYFo3WA1QAAIAAAACAAAAAgAAAAAAAAAAAAAAiAgL1DWeV+AfIP5RRB5zHv5vuXsIt8+rF9rrsji3FhQlhzBgWjdYDVAAAgAAAAIAAAACAAQAAAAAAAAAA',
);
});
it('can combine signed PSBT and prepare it for broadcast', async () => { it('can combine signed PSBT and prepare it for broadcast', async () => {
let w = new WatchOnlyWallet(); let w = new WatchOnlyWallet();
w.setSecret('zpub6rjLjQVqVnj7crz9E4QWj4WgczmEseJq22u2B6k2HZr6NE2PQx3ZYg8BnbjN9kCfHymSeMd2EpwpM5iiz5Nrb3TzvddxW2RMcE3VXdVaXHk'); w.setSecret('zpub6rjLjQVqVnj7crz9E4QWj4WgczmEseJq22u2B6k2HZr6NE2PQx3ZYg8BnbjN9kCfHymSeMd2EpwpM5iiz5Nrb3TzvddxW2RMcE3VXdVaXHk');

View file

@ -4,12 +4,15 @@ const assert = require('assert');
describe('unit - DeepLinkSchemaMatch', function() { describe('unit - DeepLinkSchemaMatch', function() {
it('hasSchema', () => { it('hasSchema', () => {
const hasSchema = DeeplinkSchemaMatch.hasSchema('bitcoin:12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG'); assert.ok(DeeplinkSchemaMatch.hasSchema('bitcoin:12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG'));
assert.ok(hasSchema); assert.ok(DeeplinkSchemaMatch.hasSchema('bitcoin:bc1qh6tf004ty7z7un2v5ntu4mkf630545gvhs45u7?amount=666&label=Yo'));
assert.ok(DeeplinkSchemaMatch.hasSchema('bitcoin:BC1QH6TF004TY7Z7UN2V5NTU4MKF630545GVHS45U7?amount=666&label=Yo'));
}); });
it('isBitcoin Address', () => { it('isBitcoin Address', () => {
assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG')); assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG'));
assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('bc1qykcp2x3djgdtdwelxn9z4j2y956npte0a4sref'));
assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('BC1QYKCP2X3DJGDTDWELXN9Z4J2Y956NPTE0A4SREF'));
}); });
it('isLighting Invoice', () => { it('isLighting Invoice', () => {
@ -36,6 +39,11 @@ describe('unit - DeepLinkSchemaMatch', function() {
); );
}); });
it('isSafelloRedirect', () => {
assert.ok(DeeplinkSchemaMatch.isSafelloRedirect({ url: 'bluewallet:?safello-state-token=TEST' }));
assert.ok(!DeeplinkSchemaMatch.isSafelloRedirect({ url: 'bluewallet:' }));
});
it('navigationForRoute', () => { it('navigationForRoute', () => {
const event = { uri: '12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG' }; const event = { uri: '12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG' };
DeeplinkSchemaMatch.navigationRouteFor(event, navValue => { DeeplinkSchemaMatch.navigationRouteFor(event, navValue => {

View file

@ -213,7 +213,7 @@ describe('Bech32 Segwit HD (BIP84)', () => {
console.error('process.env.FAULTY_ZPUB not set, skipped'); console.error('process.env.FAULTY_ZPUB not set, skipped');
return; return;
} }
jasmine.DEFAULT_TIMEOUT_INTERVAL = 90 * 1000; jasmine.DEFAULT_TIMEOUT_INTERVAL = 200 * 1000;
let hd = new HDSegwitBech32Wallet(); let hd = new HDSegwitBech32Wallet();
hd._xpub = process.env.FAULTY_ZPUB; hd._xpub = process.env.FAULTY_ZPUB;

View file

@ -20,3 +20,49 @@ jest.mock('react-native-default-preference', () => {
set: jest.fn(), set: jest.fn(),
} }
}) })
jest.mock('react-native-fs', () => {
return {
mkdir: jest.fn(),
moveFile: jest.fn(),
copyFile: jest.fn(),
pathForBundle: jest.fn(),
pathForGroup: jest.fn(),
getFSInfo: jest.fn(),
getAllExternalFilesDirs: jest.fn(),
unlink: jest.fn(),
exists: jest.fn(),
stopDownload: jest.fn(),
resumeDownload: jest.fn(),
isResumable: jest.fn(),
stopUpload: jest.fn(),
completeHandlerIOS: jest.fn(),
readDir: jest.fn(),
readDirAssets: jest.fn(),
existsAssets: jest.fn(),
readdir: jest.fn(),
setReadable: jest.fn(),
stat: jest.fn(),
readFile: jest.fn(),
read: jest.fn(),
readFileAssets: jest.fn(),
hash: jest.fn(),
copyFileAssets: jest.fn(),
copyFileAssetsIOS: jest.fn(),
copyAssetsVideoIOS: jest.fn(),
writeFile: jest.fn(),
appendFile: jest.fn(),
write: jest.fn(),
downloadFile: jest.fn(),
uploadFiles: jest.fn(),
touch: jest.fn(),
MainBundlePath: jest.fn(),
CachesDirectoryPath: jest.fn(),
DocumentDirectoryPath: jest.fn(),
ExternalDirectoryPath: jest.fn(),
ExternalStorageDirectoryPath: jest.fn(),
TemporaryDirectoryPath: jest.fn(),
LibraryDirectoryPath: jest.fn(),
PicturesDirectoryPath: jest.fn(),
}
})