Browse Source

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

settingsui
marcosrodriguezseminole 5 years ago
parent
commit
4a5a55c254
  1. 10
      BlueComponents.js
  2. 7
      MainBottomTabs.js
  3. 2
      android/app/build.gradle
  4. 22
      android/app/src/main/AndroidManifest.xml
  5. 31
      class/abstract-hd-electrum-wallet.js
  6. 18
      class/abstract-wallet.js
  7. 1
      class/app-storage.js
  8. 73
      class/deeplinkSchemaMatch.js
  9. 101
      class/lightning-custodian-wallet.js
  10. 33
      class/walletImport.js
  11. 49
      class/watch-only-wallet.js
  12. 10
      ios/BlueWallet.xcodeproj/project.pbxproj
  13. 20
      ios/BlueWallet/BlueWalletRelease.entitlements
  14. 131
      ios/BlueWallet/Info.plist
  15. 2
      ios/BlueWalletWatch Extension/Info.plist
  16. 2
      ios/BlueWalletWatch/Info.plist
  17. 6
      ios/Podfile.lock
  18. 2
      ios/TodayExtension/Info.plist
  19. 87
      ios/fastlane/metadata/en-US/release_notes.txt
  20. 9
      loc/ZAR_Afr.js
  21. 9
      loc/ZAR_Xho.js
  22. 1
      loc/cs_CZ.js
  23. 3
      loc/da_DK.js
  24. 2
      loc/de_DE.js
  25. 1
      loc/el.js
  26. 5
      loc/en.js
  27. 3
      loc/es.js
  28. 1
      loc/fi_FI.js
  29. 3
      loc/fr_FR.js
  30. 3
      loc/hr_HR.js
  31. 4
      loc/hu_HU.js
  32. 1
      loc/id_ID.js
  33. 1
      loc/it.js
  34. 1
      loc/jp_JP.js
  35. 1
      loc/nb_NO.js
  36. 1
      loc/nl_NL.js
  37. 1
      loc/pt_BR.js
  38. 1
      loc/pt_PT.js
  39. 1
      loc/ru.js
  40. 2
      loc/sv_SE.js
  41. 1
      loc/th_TH.js
  42. 1
      loc/tr_TR.js
  43. 1
      loc/ua.js
  44. 3
      loc/vi_VN.js
  45. 2
      loc/zh_cn.js
  46. 5
      loc/zh_tw.js
  47. 584
      package-lock.json
  48. 9
      package.json
  49. 66
      screen/lnd/lndCreateInvoice.js
  50. 71
      screen/lnd/scanLndInvoice.js
  51. 50
      screen/send/create.js
  52. 58
      screen/send/details.js
  53. 156
      screen/send/psbtWithHardwareWallet.js
  54. 56
      screen/send/scanQrAddress.js
  55. 2
      screen/transactions/RBF-create.js
  56. 328
      screen/wallets/details.js
  57. 20
      screen/wallets/import.js
  58. 16
      screen/wallets/list.js
  59. 3
      screen/wallets/selectWallet.js
  60. 58
      screen/wallets/transactions.js
  61. 58
      tests/integration/LightningCustodianWallet.test.js
  62. 18
      tests/integration/Loc.test.js
  63. 46
      tests/integration/WatchOnlyWallet.test.js
  64. 12
      tests/integration/deepLinkSchemaMatch.test.js
  65. 2
      tests/integration/hd-segwit-bech32-wallet.test.js
  66. 46
      tests/setup.js

10
BlueComponents.js

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

7
MainBottomTabs.js

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

2
android/app/build.gradle

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

22
android/app/src/main/AndroidManifest.xml

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

31
class/abstract-hd-electrum-wallet.js

@ -8,6 +8,7 @@ const BlueElectrum = require('../BlueElectrum');
const HDNode = require('bip32');
const coinSelectAccumulative = require('coinselect/accumulative');
const coinSelectSplit = require('coinselect/split');
const reverse = require('buffer-reverse');
const { RNRandomBytes } = NativeModules;
@ -635,6 +636,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
async fetchUtxo() {
// considering only confirmed balance
// also, fetching utxo of addresses that only have some balance
let addressess = [];
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 sequence {Number} Used in RBF
* @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}}
*/
createTransaction(utxos, targets, feeRate, changeAddress, sequence, skipSigning = false) {
createTransaction(utxos, targets, feeRate, changeAddress, sequence, skipSigning = false, masterFingerprint) {
if (!changeAddress) throw new Error('No change address provided');
sequence = sequence || AbstractHDElectrumWallet.defaultRBFSequence;
@ -756,7 +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');
}
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
// should be from root. basically, fingerprint should be provided from outside by user when importing zpub
let path = this._getDerivationPathByAddress(input.address);
@ -767,7 +778,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
sequence,
bip32Derivation: [
{
masterFingerprint,
masterFingerprint: masterFingerprintBuffer,
path,
pubkey,
},
@ -789,7 +800,17 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
let path = this._getDerivationPathByAddress(output.address);
let pubkey = this._getPubkeyByAddress(output.address);
let masterFingerprint = Buffer.from([0x00, 0x00, 0x00, 0x00]);
let masterFingerprintBuffer;
if (masterFingerprint) {
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
// 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) {
outputData['bip32Derivation'] = [
{
masterFingerprint,
masterFingerprint: masterFingerprintBuffer,
path,
pubkey,
},

18
class/abstract-wallet.js

@ -1,5 +1,6 @@
import { BitcoinUnit, Chain } from '../models/bitcoinUnits';
const createHash = require('create-hash');
export class AbstractWallet {
static type = 'abstract';
static typeReadable = 'abstract';
@ -128,6 +129,19 @@ export class AbstractWallet {
setSecret(newSecret) {
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;
}
@ -144,4 +158,8 @@ export class AbstractWallet {
getAddressAsync() {
return new Promise(resolve => resolve(this.getAddress()));
}
useWithHardwareWalletEnabled() {
return false;
}
}

1
class/app-storage.js

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

73
class/deeplinkSchemaMatch.js

@ -1,8 +1,12 @@
import { AppStorage, LightningCustodianWallet } from './';
import AsyncStorage from '@react-native-community/async-storage';
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 BlueApp = require('../BlueApp');
const BlueApp: AppStorage = require('../BlueApp');
class DeeplinkSchemaMatch {
static hasSchema(schemaString) {
if (typeof schemaString !== 'string' || schemaString.length <= 0) return false;
@ -30,6 +34,21 @@ class DeeplinkSchemaMatch {
if (typeof event.url !== 'string') {
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;
try {
isBothBitcoinAndLightning = DeeplinkSchemaMatch.isBothBitcoinAndLightning(event.url);
@ -40,7 +59,8 @@ class DeeplinkSchemaMatch {
completionHandler({
routeName: 'HandleOffchainAndOnChain',
params: {
onWalletSelect: this.isBothBitcoinAndLightningWalletSelect,
onWalletSelect: wallet =>
completionHandler(DeeplinkSchemaMatch.isBothBitcoinAndLightningOnWalletSelect(wallet, isBothBitcoinAndLightning)),
},
});
} else if (DeeplinkSchemaMatch.isBitcoinAddress(event.url) || BitcoinBIP70TransactionDecode.matchesPaymentURL(event.url)) {
@ -95,7 +115,7 @@ class DeeplinkSchemaMatch {
if (!haveLnWallet) {
// need to create one
let w = new LightningCustodianWallet();
w.setLabel(this.state.label || w.typeReadable);
w.setLabel(w.typeReadable);
try {
let lndhub = await AsyncStorage.getItem(AppStorage.LNDHUB);
@ -128,17 +148,14 @@ class DeeplinkSchemaMatch {
return;
}
this.navigator &&
this.navigator.dispatch(
completionHandler({
routeName: 'LappBrowser',
params: {
fromSecret: lnWallet.getSecret(),
fromWallet: lnWallet,
url: urlObject.query.url,
},
}),
);
completionHandler({
routeName: 'LappBrowser',
params: {
fromSecret: lnWallet.getSecret(),
fromWallet: lnWallet,
url: urlObject.query.url,
},
});
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) {
address = address
.replace('bitcoin:', '')

101
class/lightning-custodian-wallet.js

@ -1,5 +1,6 @@
import { LegacyWallet } from './legacy-wallet';
import Frisbee from 'frisbee';
import bolt11 from 'bolt11';
import { BitcoinUnit, Chain } from '../models/bitcoinUnits';
export class LightningCustodianWallet extends LegacyWallet {
@ -515,7 +516,7 @@ export class LightningCustodianWallet extends LegacyWallet {
* Example return:
* { destination: '03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f',
* payment_hash: 'faf996300a468b668c58ca0702a12096475a0dd2c3dde8e812f954463966bcf4',
* num_satoshisnum_satoshis: '100',
* num_satoshis: '100',
* timestamp: '1535116657',
* expiry: '3600',
* description: 'hundredSatoshis blitzhub',
@ -527,31 +528,46 @@ export class LightningCustodianWallet extends LegacyWallet {
* @param invoice BOLT invoice string
* @return {Promise.<Object>}
*/
async decodeInvoice(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));
decodeInvoice(invoice) {
let { payeeNodeKey, tags, satoshis, millisatoshis, timestamp } = bolt11.decode(invoice);
let decoded = {
destination: payeeNodeKey,
num_satoshis: satoshis ? satoshis.toString() : '0',
num_millisatoshis: millisatoshis ? millisatoshis.toString() : '0',
timestamp: timestamp.toString(),
fallback_addr: '',
route_hints: [],
};
for (let i = 0; i < tags.length; i++) {
let { tagName, data } = tags[i];
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) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
if (!decoded.expiry) decoded.expiry = '3600'; // default
if (!json.payment_hash) {
throw new Error('API unexpected response: ' + JSON.stringify(response.body));
if (parseInt(decoded.num_satoshis) === 0 && decoded.num_millisatoshis > 0) {
decoded.num_satoshis = (decoded.num_millisatoshis / 1000).toString();
}
return (this.decoded_invoice_raw = json);
return (this.decoded_invoice_raw = decoded);
}
async fetchInfo() {
@ -602,6 +618,49 @@ export class LightningCustodianWallet extends LegacyWallet {
allowReceive() {
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);
}
}
/*

33
class/walletImport.js

@ -18,24 +18,41 @@ const BlueApp = require('../BlueApp');
const loc = require('../loc');
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 {
const wallet = BlueApp.getWallets().some(wallet => wallet.getSecret() === w.secret && wallet.type !== PlaceholderWallet.type);
if (wallet) {
alert('This wallet has been previously imported.');
WalletImport.removePlaceholderWallet();
} else {
alert(loc.wallets.import.success);
ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false });
w.setLabel(loc.wallets.import.imported + ' ' + w.typeReadable);
w.setUserHasSavedExport(true);
if (additionalProperties) {
for (const [key, value] of Object.entries(additionalProperties)) {
w[key] = value;
}
}
WalletImport.removePlaceholderWallet();
BlueApp.wallets.push(w);
await BlueApp.saveToDisk();
A(A.ENUM.CREATED_WALLET);
alert(loc.wallets.import.success);
}
EV(EV.enum.WALLETS_COUNT_CHANGED);
} catch (_e) {}
} catch (e) {
alert(e);
console.log(e);
WalletImport.removePlaceholderWallet();
EV(EV.enum.WALLETS_COUNT_CHANGED);
}
}
static removePlaceholderWallet() {
@ -58,7 +75,13 @@ export default class WalletImport {
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()) {
return;
}
@ -209,7 +232,7 @@ export default class WalletImport {
if (watchOnly.valid()) {
await watchOnly.fetchTransactions();
await watchOnly.fetchBalance();
return WalletImport._saveWallet(watchOnly);
return WalletImport._saveWallet(watchOnly, additionalProperties);
}
// nope?

49
class/watch-only-wallet.js

@ -11,21 +11,26 @@ export class WatchOnlyWallet extends LegacyWallet {
constructor() {
super();
this.use_with_hardware_wallet = false;
this.masterFingerprint = false;
}
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() {
return (
!!this.use_with_hardware_wallet && this._hdWalletInstance instanceof HDSegwitBech32Wallet && this._hdWalletInstance.allowBatchSend()
this.useWithHardwareWalletEnabled() &&
this._hdWalletInstance instanceof HDSegwitBech32Wallet &&
this._hdWalletInstance.allowBatchSend()
);
}
allowSendMax() {
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 {
bitcoin.address.toOutputScript(this.getAddress());
return true;
} catch (e) {
} catch (_) {
return false;
}
}
@ -146,9 +151,43 @@ export class WatchOnlyWallet extends LegacyWallet {
*/
createTransaction(utxos, targets, feeRate, changeAddress, sequence) {
if (this._hdWalletInstance instanceof HDSegwitBech32Wallet) {
return this._hdWalletInstance.createTransaction(utxos, targets, feeRate, changeAddress, sequence, true);
return this._hdWalletInstance.createTransaction(utxos, targets, feeRate, changeAddress, sequence, true, this.getMasterFingerprint());
} else {
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;
}
}

10
ios/BlueWallet.xcodeproj/project.pbxproj

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

20
ios/BlueWallet/BlueWalletRelease.entitlements

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

131
ios/BlueWallet/Info.plist

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

2
ios/BlueWalletWatch Extension/Info.plist

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

2
ios/BlueWalletWatch/Info.plist

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

6
ios/Podfile.lock

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

2
ios/TodayExtension/Info.plist

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

87
ios/fastlane/metadata/en-US/release_notes.txt

@ -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
======
@ -10,44 +55,4 @@ v4.9.0
* FIX: Hide Manage Funds button if wallet doesn't allow onchain refill.
* FIX: LN Scan to receive is more visible
* FIX: Quick actions not appearing on non-3d touch devices.
* 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
* FIX: Dont show clipboard modal when biometrics is dismissed

9
loc/ZAR_Afr.js

@ -25,6 +25,9 @@
latest_transaction: 'laaste transaksie',
empty_txs1: 'U transaksies is hier beskikbaar,',
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',
},
reorder: {
@ -50,6 +53,7 @@
details: {
title: 'Beursiet',
address: 'AdresAddress',
master_fingerprint: 'Master fingerprint',
type: 'Tipe',
label: 'Etiket',
destination: 'bestemming',
@ -165,6 +169,7 @@
create: 'Skep',
setAmount: 'Bedrag ontvang',
},
scan_lnurl: 'Scan to receive',
},
buyBitcoin: {
header: 'Koop Bitcoin',
@ -186,10 +191,14 @@
'Om u eie LND node te konnekteer, installeer asseblief LndHub' +
' and put its URL here in settings. Leave blank om die standaard LndHub' +
'(lndhub.io) te gebruik',
electrum_settings: 'Electrum Settings',
electrum_settings_explain: 'Set to blank to use default',
save: 'stoor',
about: 'info',
language: 'Taal',
currency: 'Geldeenheid',
advanced_options: 'Advanced Options',
enable_advanced_mode: 'Enable advanced mode',
},
plausibledeniability: {
title: 'Geloofwaardige Ontkenbaarheid',

9
loc/ZAR_Xho.js

@ -23,6 +23,9 @@
latest_transaction: 'Utshintsho olutsha',
empty_txs1: 'Intengiso yakho iya kubonakala apha,',
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',
},
reorder: {
@ -48,6 +51,7 @@
details: {
title: 'Ingxowa',
address: 'Ikheli',
master_fingerprint: 'Master fingerprint',
type: 'Uhlobo',
label: 'Igama',
destination: 'ukuya kuyo',
@ -163,6 +167,7 @@
create: 'Yenza',
setAmount: 'Fumana ngexabiso',
},
scan_lnurl: 'Scan to receive',
},
buyBitcoin: {
header: 'Thenga Ibitcoin',
@ -183,10 +188,14 @@
lightning_settings_explain:
'Ukuxhuma kwi-node yakho ye-LND nceda ufake iLndHub' +
' 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',
about: 'Malunga',
language: 'Ulwimi',
currency: 'Lwemali',
advanced_options: 'Advanced Options',
enable_advanced_mode: 'Enable advanced mode',
},
plausibledeniability: {
title: 'Ukuphika',

1
loc/cs_CZ.js

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

3
loc/da_DK.js

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

2
loc/de_DE.js

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

1
loc/el.js

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

5
loc/en.js

@ -51,6 +51,7 @@ module.exports = {
details: {
title: 'Wallet',
address: 'Address',
master_fingerprint: 'Master fingerprint',
type: 'Type',
label: 'Label',
destination: 'destination',
@ -80,7 +81,7 @@ module.exports = {
error: 'Failed to import. Please, make sure that the provided data is valid.',
success: 'Success',
do_import: 'Import',
scan_qr: 'or scan QR code instead?',
scan_qr: '...scan QR or import file instead?',
},
scanQrWif: {
go_back: 'Go Back',
@ -145,7 +146,7 @@ module.exports = {
title: 'create transaction',
error: 'Error creating transaction. Invalid address or send amount?',
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',
amount: 'Amount',
fee: 'Fee',

3
loc/es.js

@ -35,7 +35,7 @@ module.exports = {
title: 'Añadir billetera',
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.',
scan: 'Escaniar',
scan: 'Escanear',
create: 'Crear',
label_new_segwit: 'Nuevo SegWit',
label_new_lightning: 'Nuevo Lightning',
@ -51,6 +51,7 @@ module.exports = {
details: {
title: 'Detalles de la billetera',
address: 'Dirección',
master_fingerprint: 'Master fingerprint',
type: 'Tipo',
label: 'Etiqueta',
delete: 'Eliminar',

1
loc/fi_FI.js

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

3
loc/fr_FR.js

@ -52,6 +52,7 @@ module.exports = {
details: {
title: 'Portefeuille',
address: 'Adresse',
master_fingerprint: 'Master fingerprint',
type: 'Type',
label: 'Libelé',
destination: 'destination',
@ -146,7 +147,7 @@ module.exports = {
title: 'créer une transaction',
error: 'Erreur creating transaction. Invalid address or send amount?',
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: 'À',
amount: 'Montant',
fee: 'Frais',

3
loc/hr_HR.js

@ -10,6 +10,8 @@ module.exports = {
wallets: {
select_wallet: 'Odaberi volet',
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: {
app_name: 'BlueWallet',
title: 'Voleti',
@ -48,6 +50,7 @@ module.exports = {
details: {
title: 'Volet',
address: 'Adresa',
master_fingerprint: 'Master fingerprint',
type: 'Tip',
label: 'Oznaka',
destination: 'odredište',

4
loc/hu_HU.js

@ -51,8 +51,10 @@ module.exports = {
details: {
title: 'Tárca',
address: 'Cím',
master_fingerprint: 'Master fingerprint',
type: 'Típus',
label: 'Cimke',
destination: 'destination',
description: 'leírás',
are_you_sure: 'Biztos vagy benne?',
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' +
' é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.',
electrum_settings: 'Electrum Settings',
electrum_settings_explain: 'Set to blank to use default',
save: 'Ment',
about: 'Egyéb',
language: 'Nyelv',

1
loc/id_ID.js

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

1
loc/it.js

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

1
loc/jp_JP.js

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

1
loc/nb_NO.js

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

1
loc/nl_NL.js

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

1
loc/pt_BR.js

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

1
loc/pt_PT.js

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

1
loc/ru.js

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

2
loc/sv_SE.js

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

1
loc/th_TH.js

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

1
loc/tr_TR.js

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

1
loc/ua.js

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

3
loc/vi_VN.js

@ -51,6 +51,7 @@ module.exports = {
details: {
title: 'Wallet',
address: 'Address',
master_fingerprint: 'Master fingerprint',
type: 'Type',
label: 'Label',
destination: 'destination',
@ -145,7 +146,7 @@ module.exports = {
title: 'create transaction',
error: 'Error creating transaction. Invalid address or send amount?',
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',
amount: 'Amount',
fee: 'Fee',

2
loc/zh_cn.js

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

5
loc/zh_tw.js

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

584
package-lock.json

File diff suppressed because it is too large

9
package.json

@ -1,6 +1,6 @@
{
"name": "BlueWallet",
"version": "4.9.1",
"version": "5.0.0",
"devDependencies": {
"@babel/core": "^7.5.0",
"@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",
"test": "npm run unit && npm run jest && npm run lint",
"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/*"
},
"jest": {
@ -64,6 +65,7 @@
"bip32": "2.0.3",
"bip39": "2.5.0",
"bitcoinjs-lib": "5.1.6",
"bolt11": "1.2.7",
"buffer": "5.2.1",
"buffer-reverse": "1.0.1",
"coinselect": "3.1.11",
@ -71,7 +73,7 @@
"dayjs": "1.8.14",
"ecurve": "1.0.6",
"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-react": "7.0.2",
"eslint-plugin-prettier": "3.1.0",
@ -93,6 +95,7 @@
"react-native-camera": "3.4.0",
"react-native-default-preference": "1.4.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-flexi-radio-button": "0.2.2",
"react-native-fs": "2.13.3",

66
screen/lnd/lndCreateInvoice.js

@ -20,7 +20,7 @@ import {
import { LightningCustodianWallet } from '../../class/lightning-custodian-wallet';
import PropTypes from 'prop-types';
import bech32 from 'bech32';
import { BitcoinUnit } from '../../models/bitcoinUnits';
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import NavigationService from '../../NavigationService';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import { Icon } from 'react-native-elements';
@ -36,7 +36,8 @@ export default class LNDCreateInvoice extends Component {
constructor(props) {
super(props);
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this._keyboardDidShow);
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this._keyboardDidHide);
let fromWallet;
if (props.navigation.state.params.fromWallet) fromWallet = props.navigation.getParam('fromWallet');
@ -56,6 +57,7 @@ export default class LNDCreateInvoice extends Component {
lnurl: '',
lnurlParams: null,
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() {
this.setState({ isLoading: true }, async () => {
try {
@ -195,7 +210,10 @@ export default class LNDCreateInvoice extends Component {
<TouchableOpacity
disabled={this.state.isLoading}
onPress={() => {
NavigationService.navigate('ScanQrAddress', { onBarScanned: this.processLnurl });
NavigationService.navigate('ScanQrAddress', {
onBarScanned: this.processLnurl,
launchedBy: this.props.navigation.state.routeName,
});
Keyboard.dismiss();
}}
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() {
if (!this.state.fromWallet) {
return (
@ -283,6 +340,7 @@ export default class LNDCreateInvoice extends Component {
{this.renderCreateButton()}
</KeyboardAvoidingView>
</View>
{this.renderWalletSelectionButton()}
</View>
</TouchableWithoutFeedback>
);
@ -295,7 +353,9 @@ LNDCreateInvoice.propTypes = {
dismiss: PropTypes.func,
navigate: PropTypes.func,
getParam: PropTypes.func,
pop: PropTypes.func,
state: PropTypes.shape({
routeName: PropTypes.string,
params: PropTypes.shape({
uri: PropTypes.string,
fromWallet: PropTypes.shape({}),

71
screen/lnd/scanLndInvoice.js

@ -80,33 +80,9 @@ export default class ScanLndInvoice extends React.Component {
}
}
componentDidMount() {
if (this.props.navigation.state.params.uri) {
this.processTextForInvoice(this.props.navigation.getParam('uri'));
}
}
componentWillUnmount() {
this.keyboardDidShowListener.remove();
this.keyboardDidHideListener.remove();
}
_keyboardDidShow = () => {
this.setState({ renderWalletSelectionButtonHidden: true });
};
_keyboardDidHide = () => {
this.setState({ renderWalletSelectionButtonHidden: false });
};
processInvoice = data => {
this.setState({ isLoading: true }, async () => {
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();
}
static getDerivedStateFromProps(props, state) {
if (props.navigation.state.params.uri) {
let data = props.navigation.state.params.uri;
// handling BIP21 w/BOLT11 support
let ind = data.indexOf('lightning=');
if (ind !== -1) {
@ -119,10 +95,10 @@ export default class ScanLndInvoice extends React.Component {
/**
* @type {LightningCustodianWallet}
*/
let w = this.state.fromWallet;
let w = state.fromWallet;
let decoded;
try {
decoded = await w.decodeInvoice(data);
decoded = w.decodeInvoice(data);
let expiresIn = (decoded.timestamp * 1 + decoded.expiry * 1) * 1000; // ms
if (+new Date() > expiresIn) {
@ -131,21 +107,41 @@ export default class ScanLndInvoice extends React.Component {
expiresIn = Math.round((expiresIn - +new Date()) / (60 * 1000)) + ' min';
}
Keyboard.dismiss();
this.setState({
props.navigation.setParams({ uri: undefined });
return {
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);
Keyboard.dismiss();
props.navigation.setParams({ uri: undefined });
setTimeout(() => alert(Err.message), 10);
return { ...state, isLoading: false };
}
});
}
return state;
}
componentWillUnmount() {
this.keyboardDidShowListener.remove();
this.keyboardDidHideListener.remove();
}
_keyboardDidShow = () => {
this.setState({ renderWalletSelectionButtonHidden: true });
};
_keyboardDidHide = () => {
this.setState({ renderWalletSelectionButtonHidden: false });
};
processInvoice = data => {
this.props.navigation.setParams({ uri: data });
};
async pay() {
@ -216,7 +212,7 @@ export default class ScanLndInvoice extends React.Component {
if (typeof this.state.decoded !== 'object') {
return true;
} else {
if (!this.state.decoded.hasOwnProperty('num_satoshis')) {
if (!this.state.decoded.num_satoshis) {
return true;
}
}
@ -295,7 +291,7 @@ export default class ScanLndInvoice extends React.Component {
<BlueCard>
<BlueAddressInput
onChangeText={text => {
this.setState({ destination: text });
text = text.trim();
this.processTextForInvoice(text);
}}
onBarScanned={this.processInvoice}
@ -355,6 +351,7 @@ ScanLndInvoice.propTypes = {
navigate: PropTypes.func,
pop: PropTypes.func,
getParam: PropTypes.func,
setParams: PropTypes.func,
dismiss: PropTypes.func,
state: PropTypes.shape({
routeName: PropTypes.string,

50
screen/send/create.js

@ -1,3 +1,4 @@
/* global alert */
import React, { Component } from 'react';
import {
TextInput,
@ -11,26 +12,36 @@ import {
Keyboard,
Text,
View,
Platform,
PermissionsAndroid,
} from 'react-native';
import { BlueNavigationStyle, SafeBlueArea, BlueCard, BlueText } from '../../BlueComponents';
import PropTypes from 'prop-types';
import Privacy from '../../Privacy';
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} */
const BlueApp = require('../../BlueApp');
const loc = require('../../loc');
const currency = require('../../currency');
export default class SendCreate extends Component {
static navigationOptions = () => ({
static navigationOptions = ({ navigation }) => ({
...BlueNavigationStyle,
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) {
super(props);
console.log('send/create constructor');
props.navigation.setParams({ exportTXN: this.exportTXN });
this.state = {
isLoading: false,
fee: props.navigation.getParam('fee'),
@ -44,11 +55,43 @@ export default class SendCreate extends Component {
};
}
async componentDidMount() {
componentDidMount() {
Privacy.enableBlur();
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() {
Privacy.disableBlur();
}
@ -164,6 +207,7 @@ const styles = StyleSheet.create({
SendCreate.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.func,
setParams: PropTypes.func,
getParam: PropTypes.func,
navigate: PropTypes.func,
dismiss: PropTypes.func,

58
screen/send/details.js

@ -38,6 +38,9 @@ import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import { HDLegacyP2PKHWallet, HDSegwitBech32Wallet, HDSegwitP2SHWallet, LightningCustodianWallet, WatchOnlyWallet } from '../../class';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import { BitcoinTransaction } from '../../models/bitcoinTransactionInfo';
import DocumentPicker from 'react-native-document-picker';
import RNFS from 'react-native-fs';
import DeeplinkSchemaMatch from '../../class/deeplinkSchemaMatch';
const bitcoin = require('bitcoinjs-lib');
const bip21 = require('bip21');
let BigNumber = require('bignumber.js');
@ -135,7 +138,10 @@ export default class SendDetails extends Component {
} else {
let recipients = this.state.addresses;
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;
this.setState({
address: recipients,
@ -161,7 +167,7 @@ export default class SendDetails extends Component {
this.setState({ isLoading: false });
}
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]].amount = options.amount;
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 = () => {
const isSendMaxUsed = this.state.addresses.some(element => element.amount === BitcoinUnit.MAX);
return (
@ -738,6 +787,11 @@ export default class SendDetails extends Component {
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() && (
<>
<BlueListItem

156
screen/send/psbtWithHardwareWallet.js

@ -1,6 +1,18 @@
/* global alert */
import React, { Component } from 'react';
import { ActivityIndicator, TouchableOpacity, View, Dimensions, Image, TextInput, Clipboard, Linking } from 'react-native';
import {
ActivityIndicator,
TouchableOpacity,
ScrollView,
View,
Dimensions,
Image,
TextInput,
Clipboard,
Linking,
Platform,
PermissionsAndroid,
} from 'react-native';
import QRCode from 'react-native-qrcode-svg';
import { Icon, Text } from 'react-native-elements';
import {
@ -13,8 +25,11 @@ import {
BlueCopyToClipboardButton,
} from '../../BlueComponents';
import PropTypes from 'prop-types';
import Share from 'react-native-share';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import { RNCamera } from 'react-native-camera';
import RNFS from 'react-native-fs';
import DocumentPicker from 'react-native-document-picker';
let loc = require('../../loc');
let EV = require('../../events');
let BlueElectrum = require('../../BlueElectrum');
@ -33,10 +48,19 @@ export default class PsbtWithHardwareWallet extends Component {
onBarCodeRead = ret => {
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 }, () => {
console.log(ret.data);
try {
let Tx = this.state.fromWallet.combinePsbt(this.state.psbt.toBase64(), ret.data);
let Tx = this.state.fromWallet.combinePsbt(
this.state.isFirstPSBTAlreadyBase64 ? this.state.psbt : this.state.psbt.toBase64(),
ret.data,
);
this.setState({ txhex: Tx.toHex() });
} catch (Err) {
alert(Err);
@ -46,18 +70,47 @@ export default class PsbtWithHardwareWallet extends Component {
constructor(props) {
super(props);
this.state = {
isLoading: false,
renderScanner: false,
qrCodeHeight: height > width ? width - 40 : width / 2,
qrCodeHeight: height > width ? width - 40 : width / 3,
memo: props.navigation.getParam('memo'),
psbt: props.navigation.getParam('psbt'),
fromWallet: props.navigation.getParam('fromWallet'),
isFirstPSBTAlreadyBase64: props.navigation.getParam('isFirstPSBTAlreadyBase64'),
isSecondPSBTAlreadyBase64: false,
deepLinkPSBT: undefined,
txhex: props.navigation.getParam('txhex') || undefined,
};
this.fileName = `${Date.now()}.psbt`;
}
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;
}
async componentDidMount() {
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() {
if (this.state.isLoading) {
return (
@ -200,27 +303,58 @@ export default class PsbtWithHardwareWallet extends Component {
return (
<SafeBlueArea style={{ flex: 1 }}>
<View style={{ alignItems: 'center', justifyContent: 'space-between' }}>
<ScrollView centerContent contentContainerStyle={{ flexGrow: 1, justifyContent: 'space-between' }}>
<View style={{ flexDirection: 'row', justifyContent: 'center', paddingTop: 16, paddingBottom: 16 }}>
<BlueCard>
<BlueText>This is partially signed bitcoin transaction (PSBT). Please finish signing it with your hardware wallet.</BlueText>
<BlueSpacing20 />
<QRCode
value={this.state.psbt.toBase64()}
value={this.state.isFirstPSBTAlreadyBase64 ? this.state.psbt : this.state.psbt.toBase64()}
size={this.state.qrCodeHeight}
color={BlueApp.settings.foregroundColor}
logoBackgroundColor={BlueApp.settings.brandingColor}
ecl={'L'}
/>
<BlueSpacing20 />
<BlueButton onPress={() => this.setState({ renderScanner: true })} title={'Scan signed transaction'} />
<BlueButton
icon={{
name: 'qrcode',
type: 'font-awesome',
color: BlueApp.settings.buttonTextColor,
}}
onPress={() => this.setState({ renderScanner: true })}
title={'Scan Signed Transaction'}
/>
<BlueSpacing20 />
<BlueButton
icon={{
name: 'file-import',
type: 'material-community',
color: BlueApp.settings.buttonTextColor,
}}
onPress={this.openSignedTransaction}
title={'Open Signed Transaction'}
/>
<BlueSpacing20 />
<BlueButton
icon={{
name: 'share-alternative',
type: 'entypo',
color: BlueApp.settings.buttonTextColor,
}}
onPress={this.exportPSBT}
title={'Export to file'}
/>
<BlueSpacing20 />
<View style={{ justifyContent: 'center', alignItems: 'center' }}>
<BlueCopyToClipboardButton stringToCopy={this.state.psbt.toBase64()} displayText={'Copy to Clipboard'} />
<BlueCopyToClipboardButton
stringToCopy={this.state.isFirstPSBTAlreadyBase64 ? this.state.psbt : this.state.psbt.toBase64()}
displayText={'Copy to Clipboard'}
/>
</View>
</BlueCard>
</View>
</View>
</ScrollView>
</SafeBlueArea>
);
}

56
screen/send/scanQrAddress.js

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

2
screen/transactions/RBF-create.js

@ -203,7 +203,7 @@ export default class SendCreate extends Component {
<SafeBlueArea style={{ flex: 1, paddingTop: 20 }}>
<BlueSpacing />
<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
style={{

328
screen/wallets/details.js

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

20
screen/wallets/import.js

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

16
screen/wallets/list.js

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

3
screen/wallets/selectWallet.js

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

58
screen/wallets/transactions.js

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

58
tests/integration/LightningCustodianWallet.test.js

@ -7,7 +7,7 @@ describe('LightningCustodianWallet', () => {
let l1 = new LightningCustodianWallet();
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._refresh_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 () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000;
jasmine.DEFAULT_TIMEOUT_INTERVAL = 200 * 1000;
assert.ok(l1.refill_addressess.length === 0);
assert.ok(l1._refresh_token_created_ts === 0);
assert.ok(l1._access_token_created_ts === 0);
@ -51,7 +51,7 @@ describe('LightningCustodianWallet', () => {
});
it('can refresh token', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000;
jasmine.DEFAULT_TIMEOUT_INTERVAL = 200 * 1000;
let oldRefreshToken = l1.refresh_token;
let oldAccessToken = l1.access_token;
await l1.refreshAcessToken();
@ -62,7 +62,7 @@ describe('LightningCustodianWallet', () => {
});
it('can use existing login/pass', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000;
jasmine.DEFAULT_TIMEOUT_INTERVAL = 200 * 1000;
if (!process.env.BLITZHUB) {
console.error('process.env.BLITZHUB not set, skipped');
return;
@ -100,11 +100,12 @@ describe('LightningCustodianWallet', () => {
let invoice =
'lnbc1u1pdcqpt3pp5ltuevvq2g69kdrzcegrs9gfqjer45rwjc0w736qjl92yvwtxhn6qdp8dp6kuerjv4j9xct5daeks6tnyp3xc6t50f582cscqp2zrkghzl535xjav52ns0rpskcn20takzdr2e02wn4xqretlgdemg596acq5qtfqhjk4jpr7jk8qfuuka2k0lfwjsk9mchwhxcgxzj3tsp09gfpy';
let decoded = await l2.decodeInvoice(invoice);
let decoded = l2.decodeInvoice(invoice);
assert.ok(decoded.payment_hash);
assert.ok(decoded.description);
assert.ok(decoded.num_satoshis);
assert.strictEqual(parseInt(decoded.num_satoshis) * 1000, parseInt(decoded.num_millisatoshis));
await l2.checkRouteInvoice(invoice);
@ -112,15 +113,44 @@ describe('LightningCustodianWallet', () => {
invoice = 'gsom';
let error = false;
try {
await l2.decodeInvoice(invoice);
l2.decodeInvoice(invoice);
} catch (Err) {
error = true;
}
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 () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000;
jasmine.DEFAULT_TIMEOUT_INTERVAL = 200 * 1000;
if (!process.env.BLITZHUB) {
console.error('process.env.BLITZHUB not set, skipped');
return;
@ -155,7 +185,7 @@ describe('LightningCustodianWallet', () => {
await l2.fetchTransactions();
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.description);
@ -194,7 +224,7 @@ describe('LightningCustodianWallet', () => {
});
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) {
console.error('process.env.BLITZHUB not set, skipped');
return;
@ -294,7 +324,7 @@ describe('LightningCustodianWallet', () => {
});
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) {
console.error('process.env.BLITZHUB not set, skipped');
return;
@ -336,7 +366,7 @@ describe('LightningCustodianWallet', () => {
let oldBalance = +l2.balance;
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.description);
assert.strictEqual(+decoded.num_satoshis, 0);
@ -371,7 +401,7 @@ describe('LightningCustodianWallet', () => {
it('cant create zemo amt invoices yet', async () => {
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._refresh_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 () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000;
jasmine.DEFAULT_TIMEOUT_INTERVAL = 200 * 1000;
if (!process.env.BLITZHUB) {
console.error('process.env.BLITZHUB not set, skipped');
return;
@ -443,7 +473,7 @@ describe('LightningCustodianWallet', () => {
let oldBalance = +l2.balance;
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.description);
assert.strictEqual(+decoded.num_satoshis, 0);

18
tests/integration/Loc.test.js

@ -1,24 +1,28 @@
/* global it, describe */
let assert = require('assert');
const fs = require('fs');
describe('Localization', () => {
it('has all keys in all locales', async () => {
let en = require('../../loc/en');
let noErrors = true;
let issues = 0;
for (let key1 of Object.keys(en)) {
for (let key2 of Object.keys(en[key1])) {
// 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);
if (typeof locale[key1] === 'undefined') {
console.error('Missing: ' + lang + '.' + key1);
noErrors = false;
issues++;
} else if (typeof locale[key1][key2] === 'undefined') {
console.error('Missing: ' + lang + '.' + key1 + '.' + key2);
noErrors = false;
issues++;
}
// 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])) {
if (typeof locale[key1][key2][key3] === 'undefined') {
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);
});
});

46
tests/integration/WatchOnlyWallet.test.js

@ -56,8 +56,10 @@ describe('Watch only wallet', () => {
let w = new WatchOnlyWallet();
w.setSecret('12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG');
assert.ok(w.valid());
assert.strictEqual(w.isHd(), false);
w.setSecret('3BDsBDxDimYgNZzsqszNZobqQq3yeUoJf2');
assert.ok(w.valid());
assert.strictEqual(w.isHd(), false);
w.setSecret('not valid');
assert.ok(!w.valid());
@ -67,6 +69,9 @@ describe('Watch only wallet', () => {
assert.ok(w.valid());
w.setSecret('zpub6r7jhKKm7BAVx3b3nSnuadY1WnshZYkhK8gKFoRLwK9rF3Mzv28BrGcCGA3ugGtawi1WLb2vyjQAX9ZTDGU5gNk2bLdTc3iEXr6tzR1ipNP');
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 () => {
@ -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 () => {
let w = new WatchOnlyWallet();
w.setSecret('zpub6rjLjQVqVnj7crz9E4QWj4WgczmEseJq22u2B6k2HZr6NE2PQx3ZYg8BnbjN9kCfHymSeMd2EpwpM5iiz5Nrb3TzvddxW2RMcE3VXdVaXHk');

12
tests/integration/deepLinkSchemaMatch.test.js

@ -4,12 +4,15 @@ const assert = require('assert');
describe('unit - DeepLinkSchemaMatch', function() {
it('hasSchema', () => {
const hasSchema = DeeplinkSchemaMatch.hasSchema('bitcoin:12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG');
assert.ok(hasSchema);
assert.ok(DeeplinkSchemaMatch.hasSchema('bitcoin:12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG'));
assert.ok(DeeplinkSchemaMatch.hasSchema('bitcoin:bc1qh6tf004ty7z7un2v5ntu4mkf630545gvhs45u7?amount=666&label=Yo'));
assert.ok(DeeplinkSchemaMatch.hasSchema('bitcoin:BC1QH6TF004TY7Z7UN2V5NTU4MKF630545GVHS45U7?amount=666&label=Yo'));
});
it('isBitcoin Address', () => {
assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG'));
assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('bc1qykcp2x3djgdtdwelxn9z4j2y956npte0a4sref'));
assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('BC1QYKCP2X3DJGDTDWELXN9Z4J2Y956NPTE0A4SREF'));
});
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', () => {
const event = { uri: '12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG' };
DeeplinkSchemaMatch.navigationRouteFor(event, navValue => {

2
tests/integration/hd-segwit-bech32-wallet.test.js

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

46
tests/setup.js

@ -19,4 +19,50 @@ jest.mock('react-native-default-preference', () => {
setName: 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(),
}
})
Loading…
Cancel
Save