Browse Source

Merge branch 'master' into settingsui

settingsui
marcosrdz 5 years ago
parent
commit
d4b27bb73c
  1. 20
      .circleci/config.yml
  2. 2
      BlueComponents.js
  3. 4
      BlueElectrum.js
  4. 14
      MainBottomTabs.js
  5. 2
      android/app/build.gradle
  6. 40
      class/abstract-hd-electrum-wallet.js
  7. 3
      class/abstract-hd-wallet.js
  8. 9
      class/abstract-wallet.js
  9. 45
      class/hd-legacy-breadwallet-wallet.js
  10. 4
      class/hd-legacy-p2pkh-wallet.js
  11. 4
      class/hd-segwit-bech32-wallet.js
  12. 399
      class/legacy-wallet.js
  13. 4
      class/segwit-bech-wallet.js
  14. 4
      class/segwit-p2sh-wallet.js
  15. 10
      class/walletImport.js
  16. 2
      ios/BlueWallet/Info.plist
  17. 2
      ios/BlueWalletWatch Extension/Info.plist
  18. 2
      ios/BlueWalletWatch/Info.plist
  19. 60
      ios/Podfile.lock
  20. 2
      ios/TodayExtension/Info.plist
  21. 32
      ios/fastlane/metadata/en-US/release_notes.txt
  22. 42
      loc/de_DE.js
  23. 2292
      package-lock.json
  24. 44
      package.json
  25. 5
      screen/lnd/lndCreateInvoice.js
  26. 3
      screen/receive/details.js
  27. 19
      screen/send/ScanQRCode.js
  28. 8
      screen/settings/about.js
  29. 255
      screen/transactions/RBF-create.js
  30. 202
      screen/transactions/RBF.js
  31. 26
      screen/transactions/transactionStatus.js
  32. 57
      screen/wallets/add.js
  33. 3
      screen/wallets/details.js
  34. 19
      screen/wallets/export.js
  35. 2
      screen/wallets/import.js
  36. 2
      screen/wallets/list.js
  37. 3
      screen/wallets/transactions.js
  38. 110
      tests/integration/App.test.js
  39. 30
      tests/integration/HDWallet.test.js
  40. 154
      tests/integration/LegacyWallet.test.js
  41. 76
      tests/integration/WatchOnlyWallet.test.js

20
.circleci/config.yml

@ -1,25 +1,23 @@
# Javascript Node CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-javascript/ for more details
#
version: 2
jobs:
build:
docker:
# specify the version you desire here
- image: circleci/node:8-stretch
# Specify service dependencies here if necessary
# CircleCI maintains a library of pre-built images
# documented at https://circleci.com/docs/2.0/circleci-images/
# - image: circleci/mongo:3.4.4
- image: circleci/node:10.16.3
working_directory: ~/repo
steps:
- checkout
- restore_cache:
key: node_modules-{{ checksum "package-lock.json" }}
- run: npm i
- save_cache:
key: node_modules-{{ checksum "package-lock.json" }}
paths:
- node_modules
# run tests!
- run: npm run test

2
BlueComponents.js

@ -2089,7 +2089,7 @@ export class BlueAddressInput extends Component {
<TouchableOpacity
disabled={this.props.isLoading}
onPress={() => {
NavigationService.navigate('ScanQrAddress', { onBarScanned: this.props.onBarScanned, launchedBy: this.props.launchedBy });
NavigationService.navigate('ScanQRCode', { onBarScanned: this.props.onBarScanned, launchedBy: this.props.launchedBy });
Keyboard.dismiss();
}}
style={{

4
BlueElectrum.js

@ -295,7 +295,7 @@ module.exports.multiGetHistoryByAddress = async function(addresses, batchsize) {
};
module.exports.multiGetTransactionByTxid = async function(txids, batchsize, verbose) {
batchsize = batchsize || 81;
batchsize = batchsize || 61;
// this value is fine-tuned so althrough wallets in test suite will occasionally
// throw 'response too large (over 1,000,000 bytes', test suite will pass
verbose = verbose !== false;
@ -341,7 +341,7 @@ module.exports.waitTillConnected = async function() {
clearInterval(waitTillConnectedInterval);
reject(new Error('Waiting for Electrum connection timeout'));
}
}, 1000);
}, 500);
});
};

14
MainBottomTabs.js

@ -28,8 +28,6 @@ import SelectWallet from './screen/wallets/selectWallet';
import details from './screen/transactions/details';
import TransactionStatus from './screen/transactions/transactionStatus';
import rbf from './screen/transactions/RBF';
import createrbf from './screen/transactions/RBF-create';
import cpfp from './screen/transactions/CPFP';
import rbfBumpFee from './screen/transactions/RBFBumpFee';
import rbfCancel from './screen/transactions/RBFCancel';
@ -37,7 +35,7 @@ import rbfCancel from './screen/transactions/RBFCancel';
import receiveDetails from './screen/receive/details';
import sendDetails from './screen/send/details';
import ScanQRCode from './screen/send/scanQrAddress';
import ScanQRCode from './screen/send/ScanQRCode';
import sendCreate from './screen/send/create';
import Confirm from './screen/send/confirm';
import PsbtWithHardwareWallet from './screen/send/psbtWithHardwareWallet';
@ -78,12 +76,6 @@ const WalletsStackNavigator = createStackNavigator(
WalletDetails: {
screen: WalletDetails,
},
RBF: {
screen: rbf,
},
CreateRBF: {
screen: createrbf,
},
CPFP: {
screen: cpfp,
},
@ -259,7 +251,7 @@ const HandleOffchainAndOnChainStackNavigator = createStackNavigator(
header: null,
},
},
ScanQrAddress: {
ScanQRCode: {
screen: ScanQRCode,
},
SendDetails: {
@ -330,7 +322,7 @@ const MainBottomTabs = createStackNavigator(
header: null,
},
},
ScanQrAddress: {
ScanQRCode: {
screen: ScanQRCode,
},
LappBrowser: {

2
android/app/build.gradle

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

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

@ -635,22 +635,39 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
}
async fetchUtxo() {
// considering only confirmed balance
// also, fetching utxo of addresses that only have some balance
// fetching utxo of addresses that only have some balance
let addressess = [];
// considering confirmed balance:
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
if (this._balances_by_external_index[c] && this._balances_by_external_index[c].c && this._balances_by_external_index[c].c > 0) {
addressess.push(this._getExternalAddressByIndex(c));
}
}
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
if (this._balances_by_internal_index[c] && this._balances_by_internal_index[c].c && this._balances_by_internal_index[c].c > 0) {
addressess.push(this._getInternalAddressByIndex(c));
}
}
// considering UNconfirmed balance:
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
if (this._balances_by_external_index[c] && this._balances_by_external_index[c].u && this._balances_by_external_index[c].u > 0) {
addressess.push(this._getExternalAddressByIndex(c));
}
}
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
if (this._balances_by_internal_index[c] && this._balances_by_internal_index[c].u && this._balances_by_internal_index[c].u > 0) {
addressess.push(this._getInternalAddressByIndex(c));
}
}
// note: we could remove checks `.c` and `.u` to simplify code, but the resulting `addressess` array would be bigger, thus bigger batch
// to fetch (or maybe even several fetches), which is not critical but undesirable.
// anyway, result has `.confirmations` property for each utxo, so outside caller can easily filter out unconfirmed if he wants to
addressess = [...new Set(addressess)]; // deduplicate just for any case
this._utxo = [];
for (let arr of Object.values(await BlueElectrum.multiGetUtxoByAddress(addressess))) {
this._utxo = this._utxo.concat(arr);
@ -665,8 +682,25 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
u.wif = this._getWifForAddress(u.address);
u.confirmations = u.height ? 1 : 0;
}
this.utxo = this.utxo.sort((a, b) => a.amount - b.amount);
// more consistent, so txhex in unit tests wont change
}
/**
* Getter for previously fetched UTXO. For example:
* [ { height: 0,
* value: 666,
* address: 'string',
* txId: 'string',
* vout: 1,
* txid: 'string',
* amount: 666,
* wif: 'string',
* confirmations: 0 } ]
*
* @returns {[]}
*/
getUtxo() {
return this._utxo;
}

3
class/abstract-hd-wallet.js

@ -4,6 +4,9 @@ const bitcoin = require('bitcoinjs-lib');
const bip39 = require('bip39');
const BlueElectrum = require('../BlueElectrum');
/**
* @deprecated
*/
export class AbstractHDWallet extends LegacyWallet {
static type = 'abstract';
static typeReadable = 'abstract';

9
class/abstract-wallet.js

@ -105,7 +105,7 @@ export class AbstractWallet {
}
weOwnAddress(address) {
return this._address === address;
throw Error('not implemented');
}
/**
@ -128,7 +128,12 @@ export class AbstractWallet {
}
setSecret(newSecret) {
this.secret = newSecret.trim();
this.secret = newSecret
.trim()
.replace('bitcoin:', '')
.replace('BITCOIN:', '');
if (this.secret.startsWith('BC1')) this.secret = this.secret.toLowerCase();
try {
const parsedSecret = JSON.parse(this.secret);

45
class/hd-legacy-breadwallet-wallet.js

@ -1,5 +1,4 @@
import { AbstractHDWallet } from './abstract-hd-wallet';
import Frisbee from 'frisbee';
import { AbstractHDElectrumWallet } from './abstract-hd-electrum-wallet';
import bip39 from 'bip39';
const bip32 = require('bip32');
const bitcoinjs = require('bitcoinjs-lib');
@ -8,7 +7,7 @@ const bitcoinjs = require('bitcoinjs-lib');
* HD Wallet (BIP39).
* In particular, Breadwallet-compatible (Legacy addresses)
*/
export class HDLegacyBreadwalletWallet extends AbstractHDWallet {
export class HDLegacyBreadwalletWallet extends AbstractHDElectrumWallet {
static type = 'HDLegacyBreadwallet';
static typeReadable = 'HD Legacy Breadwallet (P2PKH)';
@ -35,15 +34,10 @@ export class HDLegacyBreadwalletWallet extends AbstractHDWallet {
_getExternalAddressByIndex(index) {
index = index * 1; // cast to int
if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit
const mnemonic = this.secret;
const seed = bip39.mnemonicToSeed(mnemonic);
const root = bip32.fromSeed(seed);
const path = "m/0'/0/" + index;
const child = root.derivePath(path);
const node = bitcoinjs.bip32.fromBase58(this.getXpub());
const address = bitcoinjs.payments.p2pkh({
pubkey: child.publicKey,
pubkey: node.derive(0).derive(index).publicKey,
}).address;
return (this.external_addresses_cache[index] = address);
@ -52,15 +46,10 @@ export class HDLegacyBreadwalletWallet extends AbstractHDWallet {
_getInternalAddressByIndex(index) {
index = index * 1; // cast to int
if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit
const mnemonic = this.secret;
const seed = bip39.mnemonicToSeed(mnemonic);
const root = bip32.fromSeed(seed);
const path = "m/0'/1/" + index;
const child = root.derivePath(path);
const node = bitcoinjs.bip32.fromBase58(this.getXpub());
const address = bitcoinjs.payments.p2pkh({
pubkey: child.publicKey,
pubkey: node.derive(1).derive(index).publicKey,
}).address;
return (this.internal_addresses_cache[index] = address);
@ -90,26 +79,4 @@ export class HDLegacyBreadwalletWallet extends AbstractHDWallet {
return child.keyPair.toWIF();
}
/**
* @inheritDoc
*/
async fetchBalance() {
try {
const api = new Frisbee({ baseURI: 'https://blockchain.info' });
let response = await api.get('/balance?active=' + this.getXpub());
if (response && response.body) {
for (let xpub of Object.keys(response.body)) {
this.balance = response.body[xpub].final_balance / 100000000;
}
this._lastBalanceFetch = +new Date();
} else {
throw new Error('Could not fetch balance from API: ' + response.err);
}
} catch (err) {
console.warn(err);
}
}
}

4
class/hd-legacy-p2pkh-wallet.js

@ -1,7 +1,7 @@
import { AbstractHDWallet } from './abstract-hd-wallet';
import bip39 from 'bip39';
import BigNumber from 'bignumber.js';
import signer from '../models/signer';
import { AbstractHDElectrumWallet } from './abstract-hd-electrum-wallet';
const bitcoin = require('bitcoinjs-lib');
const HDNode = require('bip32');
@ -10,7 +10,7 @@ const HDNode = require('bip32');
* In particular, BIP44 (P2PKH legacy addressess)
* @see https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
*/
export class HDLegacyP2PKHWallet extends AbstractHDWallet {
export class HDLegacyP2PKHWallet extends AbstractHDElectrumWallet {
static type = 'HDlegacyP2PKH';
static typeReadable = 'HD Legacy (BIP44 P2PKH)';

4
class/hd-segwit-bech32-wallet.js

@ -20,4 +20,8 @@ export class HDSegwitBech32Wallet extends AbstractHDElectrumWallet {
allowSendMax() {
return true;
}
allowRBF() {
return true;
}
}

399
class/legacy-wallet.js

@ -1,7 +1,5 @@
import { AbstractWallet } from './abstract-wallet';
import { SegwitBech32Wallet } from './';
import { useBlockcypherTokens } from './constants';
import Frisbee from 'frisbee';
import { HDSegwitBech32Wallet } from './';
import { NativeModules } from 'react-native';
const bitcoin = require('bitcoinjs-lib');
const { RNRandomBytes } = NativeModules;
@ -37,7 +35,7 @@ export class LegacyWallet extends AbstractWallet {
* @return {boolean}
*/
timeToRefreshTransaction() {
for (let tx of this.transactions) {
for (let tx of this.getTransactions()) {
if (tx.confirmations < 7) {
return true;
}
@ -104,24 +102,13 @@ export class LegacyWallet extends AbstractWallet {
*/
async fetchBalance() {
try {
const api = new Frisbee({
baseURI: 'https://api.blockcypher.com/v1/btc/main/addrs/',
});
let response = await api.get(
this.getAddress() + '/balance' + ((useBlockcypherTokens && '?token=' + this.getRandomBlockcypherToken()) || ''),
);
let json = response.body;
if (typeof json === 'undefined' || typeof json.final_balance === 'undefined') {
throw new Error('Could not fetch balance from API: ' + response.err + ' ' + JSON.stringify(response.body));
}
this.balance = Number(json.final_balance);
this.unconfirmed_balance = new BigNumber(json.unconfirmed_balance);
this.unconfirmed_balance = this.unconfirmed_balance.dividedBy(100000000).toString() * 1;
let balance = await BlueElectrum.getBalanceByAddress(this.getAddress());
this.balance = Number(balance.confirmed);
this.unconfirmed_balance = new BigNumber(balance.unconfirmed);
this.unconfirmed_balance = this.unconfirmed_balance.dividedBy(100000000).toString() * 1; // wtf
this._lastBalanceFetch = +new Date();
} catch (err) {
console.warn(err);
} catch (Error) {
console.warn(Error);
}
}
@ -131,230 +118,116 @@ export class LegacyWallet extends AbstractWallet {
* @return {Promise.<void>}
*/
async fetchUtxo() {
const api = new Frisbee({
baseURI: 'https://api.blockcypher.com/v1/btc/main/addrs/',
});
let response;
try {
let maxHeight = 0;
this.utxo = [];
let json;
do {
response = await api.get(
this.getAddress() +
'?limit=2000&after=' +
maxHeight +
((useBlockcypherTokens && '&token=' + this.getRandomBlockcypherToken()) || ''),
);
json = response.body;
if (typeof json === 'undefined' || typeof json.final_balance === 'undefined') {
throw new Error('Could not fetch UTXO from API' + response.err);
}
json.txrefs = json.txrefs || []; // case when source address is empty (or maxheight too high, no txs)
for (let txref of json.txrefs) {
maxHeight = Math.max(maxHeight, txref.block_height) + 1;
if (typeof txref.spent !== 'undefined' && txref.spent === false) {
this.utxo.push(txref);
}
}
} while (json.txrefs.length);
let utxos = await BlueElectrum.multiGetUtxoByAddress([this.getAddress()]);
for (let arr of Object.values(utxos)) {
this.utxo = this.utxo.concat(arr);
}
} catch (Error) {
console.warn(Error);
}
json.unconfirmed_txrefs = json.unconfirmed_txrefs || [];
this.utxo = this.utxo.concat(json.unconfirmed_txrefs);
} catch (err) {
console.warn(err);
// backward compatibility
for (let u of this.utxo) {
u.tx_output_n = u.vout;
u.tx_hash = u.txId;
u.confirmations = u.height ? 1 : 0;
}
}
getUtxo() {
return this.utxo;
}
/**
* Fetches transactions via API. Returns VOID.
* Use getter to get the actual list.
* Fetches transactions via Electrum. Returns VOID.
* Use getter to get the actual list. *
* @see AbstractHDElectrumWallet.fetchTransactions()
*
* @return {Promise.<void>}
*/
async fetchTransactions() {
try {
const api = new Frisbee({
baseURI: 'https://api.blockcypher.com/',
});
let after = 0;
let before = 100500100;
for (let oldTx of this.getTransactions()) {
if (oldTx.block_height && oldTx.confirmations < 7) {
after = Math.max(after, oldTx.block_height);
}
// Below is a simplified copypaste from HD electrum wallet
this._txs_by_external_index = [];
let addresses2fetch = [this.getAddress()];
// first: batch fetch for all addresses histories
let histories = await BlueElectrum.multiGetHistoryByAddress(addresses2fetch);
let txs = {};
for (let history of Object.values(histories)) {
for (let tx of history) {
txs[tx.tx_hash] = tx;
}
}
while (1) {
let response = await api.get(
'v1/btc/main/addrs/' +
this.getAddress() +
'/full?after=' +
after +
'&before=' +
before +
'&limit=50' +
((useBlockcypherTokens && '&token=' + this.getRandomBlockcypherToken()) || ''),
);
let json = response.body;
if (typeof json === 'undefined' || !json.txs) {
throw new Error('Could not fetch transactions from API:' + response.err);
}
let alreadyFetchedTransactions = this.transactions;
this.transactions = json.txs;
this._lastTxFetch = +new Date();
// now, calculating value per each transaction...
for (let tx of this.transactions) {
if (tx.block_height) {
before = Math.min(before, tx.block_height); // so next time we fetch older TXs
}
// now, if we dont have enough outputs or inputs in response we should collect them from API:
if (tx.next_outputs) {
let newOutputs = await this._fetchAdditionalOutputs(tx.next_outputs);
tx.outputs = tx.outputs.concat(newOutputs);
}
if (tx.next_inputs) {
let newInputs = await this._fetchAdditionalInputs(tx.next_inputs);
tx.inputs = tx.inputs.concat(newInputs);
}
// how much came in...
let value = 0;
for (let out of tx.outputs) {
if (out && out.addresses && out.addresses.indexOf(this.getAddress()) !== -1) {
// found our address in outs of this TX
value += out.value;
}
}
tx.value = value;
// end
// how much came out
value = 0;
for (let inp of tx.inputs) {
if (!inp.addresses) {
// console.log('inp.addresses empty');
// console.log('got witness', inp.witness); // TODO
inp.addresses = [];
if (inp.witness && inp.witness[1]) {
let address = SegwitBech32Wallet.witnessToAddress(inp.witness[1]);
inp.addresses.push(address);
} else {
inp.addresses.push('???');
}
}
if (inp && inp.addresses && inp.addresses.indexOf(this.getAddress()) !== -1) {
// found our address in outs of this TX
value -= inp.output_value;
}
}
tx.value += value;
// end
}
this.transactions = alreadyFetchedTransactions.concat(this.transactions);
// next, batch fetching each txid we got
let txdatas = await BlueElectrum.multiGetTransactionByTxid(Object.keys(txs));
let txsUnconf = [];
let txs = [];
let hashPresent = {};
// now, rearranging TXs. unconfirmed go first:
for (let tx of this.transactions.reverse()) {
if (hashPresent[tx.hash]) continue;
hashPresent[tx.hash] = 1;
if (tx.block_height && tx.block_height === -1) {
// unconfirmed
console.log(tx);
if (+new Date(tx.received) < +new Date() - 3600 * 24 * 1000) {
// nop, too old unconfirmed tx - skipping it
} else {
txsUnconf.push(tx);
}
} else {
txs.push(tx);
}
}
this.transactions = txsUnconf.reverse().concat(txs.reverse());
// all reverses needed so freshly fetched TXs replace same old TXs
this.transactions = this.transactions.sort((a, b) => {
return a.received < b.received;
});
if (json.txs.length < 50) {
// final batch, so it has les than max txs
break;
// now, tricky part. we collect all transactions from inputs (vin), and batch fetch them too.
// then we combine all this data (we need inputs to see source addresses and amounts)
let vinTxids = [];
for (let txdata of Object.values(txdatas)) {
for (let vin of txdata.vin) {
vinTxids.push(vin.txid);
}
}
let vintxdatas = await BlueElectrum.multiGetTransactionByTxid(vinTxids);
// fetched all transactions from our inputs. now we need to combine it.
// iterating all _our_ transactions:
for (let txid of Object.keys(txdatas)) {
// iterating all inputs our our single transaction:
for (let inpNum = 0; inpNum < txdatas[txid].vin.length; inpNum++) {
let inpTxid = txdatas[txid].vin[inpNum].txid;
let inpVout = txdatas[txid].vin[inpNum].vout;
// got txid and output number of _previous_ transaction we shoud look into
if (vintxdatas[inpTxid] && vintxdatas[inpTxid].vout[inpVout]) {
// extracting amount & addresses from previous output and adding it to _our_ input:
txdatas[txid].vin[inpNum].addresses = vintxdatas[inpTxid].vout[inpVout].scriptPubKey.addresses;
txdatas[txid].vin[inpNum].value = vintxdatas[inpTxid].vout[inpVout].value;
}
}
} catch (err) {
console.warn(err);
}
}
async _fetchAdditionalOutputs(nextOutputs) {
let outputs = [];
let baseURI = nextOutputs.split('/');
baseURI = baseURI[0] + '/' + baseURI[1] + '/' + baseURI[2] + '/';
const api = new Frisbee({
baseURI: baseURI,
});
// now, we need to put transactions in all relevant `cells` of internal hashmaps: this.transactions_by_internal_index && this.transactions_by_external_index
do {
await (() => new Promise(resolve => setTimeout(resolve, 1000)))();
nextOutputs = nextOutputs.replace(baseURI, '');
for (let tx of Object.values(txdatas)) {
for (let vin of tx.vin) {
if (vin.addresses && vin.addresses.indexOf(this.getAddress()) !== -1) {
// this TX is related to our address
let clonedTx = Object.assign({}, tx);
clonedTx.inputs = tx.vin.slice(0);
clonedTx.outputs = tx.vout.slice(0);
delete clonedTx.vin;
delete clonedTx.vout;
let response = await api.get(nextOutputs + ((useBlockcypherTokens && '&token=' + this.getRandomBlockcypherToken()) || ''));
let json = response.body;
if (typeof json === 'undefined') {
throw new Error('Could not fetch transactions from API:' + response.err);
this._txs_by_external_index.push(clonedTx);
}
}
if (json.outputs && json.outputs.length) {
outputs = outputs.concat(json.outputs);
nextOutputs = json.next_outputs;
} else {
break;
for (let vout of tx.vout) {
if (vout.scriptPubKey.addresses.indexOf(this.getAddress()) !== -1) {
// this TX is related to our address
let clonedTx = Object.assign({}, tx);
clonedTx.inputs = tx.vin.slice(0);
clonedTx.outputs = tx.vout.slice(0);
delete clonedTx.vin;
delete clonedTx.vout;
this._txs_by_external_index.push(clonedTx);
}
}
} while (1);
}
return outputs;
this._lastTxFetch = +new Date();
}
async _fetchAdditionalInputs(nextInputs) {
let inputs = [];
let baseURI = nextInputs.split('/');
baseURI = baseURI[0] + '/' + baseURI[1] + '/' + baseURI[2] + '/';
const api = new Frisbee({
baseURI: baseURI,
});
do {
await (() => new Promise(resolve => setTimeout(resolve, 1000)))();
nextInputs = nextInputs.replace(baseURI, '');
let response = await api.get(nextInputs + ((useBlockcypherTokens && '&token=' + this.getRandomBlockcypherToken()) || ''));
let json = response.body;
if (typeof json === 'undefined') {
throw new Error('Could not fetch transactions from API:' + response.err);
}
getTransactions() {
// a hacky code reuse from electrum HD wallet:
this._txs_by_external_index = this._txs_by_external_index || [];
this._txs_by_internal_index = [];
if (json.inputs && json.inputs.length) {
inputs = inputs.concat(json.inputs);
nextInputs = json.next_inputs;
} else {
break;
}
} while (1);
return inputs;
let hd = new HDSegwitBech32Wallet();
return hd.getTransactions.apply(this);
}
async broadcastTx(txhex) {
@ -366,66 +239,8 @@ export class LegacyWallet extends AbstractWallet {
}
}
async _broadcastTxBtczen(txhex) {
const api = new Frisbee({
baseURI: 'https://btczen.com',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
});
let res = await api.get('/broadcast/' + txhex);
console.log('response btczen', res.body);
return res.body;
}
async _broadcastTxChainso(txhex) {
const api = new Frisbee({
baseURI: 'https://chain.so',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
});
let res = await api.post('/api/v2/send_tx/BTC', {
body: { tx_hex: txhex },
});
return res.body;
}
async _broadcastTxSmartbit(txhex) {
const api = new Frisbee({
baseURI: 'https://api.smartbit.com.au',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
});
let res = await api.post('/v1/blockchain/pushtx', {
body: { hex: txhex },
});
return res.body;
}
async _broadcastTxBlockcypher(txhex) {
const api = new Frisbee({
baseURI: 'https://api.blockcypher.com',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
});
let res = await api.post('/v1/btc/main/txs/push', { body: { tx: txhex } });
// console.log('blockcypher response', res);
return res.body;
}
/**
* Takes UTXOs (as presented by blockcypher api), transforms them into
* Takes UTXOs, transforms them into
* format expected by signer module, creates tx and returns signed string txhex.
*
* @param utxos Unspent outputs, expects blockcypher format
@ -462,22 +277,12 @@ export class LegacyWallet extends AbstractWallet {
return new Date(max).toString();
}
getRandomBlockcypherToken() {
return (array => {
for (let i = array.length - 1; i > 0; i--) {
let j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array[0];
})([
'0326b7107b4149559d18ce80612ef812',
'a133eb7ccacd4accb80cb1225de4b155',
'7c2b1628d27b4bd3bf8eaee7149c577f',
'f1e5a02b9ec84ec4bc8db2349022e5f5',
'e5926dbeb57145979153adc41305b183',
]);
}
/**
* Validates any address, including legacy, p2sh and bech32
*
* @param address
* @returns {boolean}
*/
isAddressValid(address) {
try {
bitcoin.address.toOutputScript(address);
@ -486,4 +291,8 @@ export class LegacyWallet extends AbstractWallet {
return false;
}
}
weOwnAddress(address) {
return this.getAddress() === address || this._address === address;
}
}

4
class/segwit-bech-wallet.js

@ -10,6 +10,10 @@ export class SegwitBech32Wallet extends LegacyWallet {
let address;
try {
let keyPair = bitcoin.ECPair.fromWIF(this.secret);
if (!keyPair.compressed) {
console.warn('only compressed public keys are good for segwit');
return false;
}
address = bitcoin.payments.p2wpkh({
pubkey: keyPair.publicKey,
}).address;

4
class/segwit-p2sh-wallet.js

@ -22,10 +22,6 @@ export class SegwitP2SHWallet extends LegacyWallet {
static type = 'segwitP2SH';
static typeReadable = 'SegWit (P2SH)';
allowRBF() {
return true;
}
static witnessToAddress(witness) {
const pubKey = Buffer.from(witness, 'hex');
return pubkeyToP2shSegwitAddress(pubKey);

10
class/walletImport.js

@ -125,7 +125,7 @@ export default class WalletImport {
if (hd4.validateMnemonic()) {
await hd4.fetchBalance();
if (hd4.getBalance() > 0) {
await hd4.fetchTransactions();
// await hd4.fetchTransactions(); // experiment: dont fetch tx now. it will import faster. user can refresh his wallet later
return WalletImport._saveWallet(hd4);
}
}
@ -168,7 +168,7 @@ export default class WalletImport {
if (hd1.validateMnemonic()) {
await hd1.fetchBalance();
if (hd1.getBalance() > 0) {
await hd1.fetchTransactions();
// await hd1.fetchTransactions(); // experiment: dont fetch tx now. it will import faster. user can refresh his wallet later
return WalletImport._saveWallet(hd1);
}
}
@ -178,7 +178,7 @@ export default class WalletImport {
if (hd2.validateMnemonic()) {
await hd2.fetchBalance();
if (hd2.getBalance() > 0) {
await hd2.fetchTransactions();
// await hd2.fetchTransactions(); // experiment: dont fetch tx now. it will import faster. user can refresh his wallet later
return WalletImport._saveWallet(hd2);
}
}
@ -188,7 +188,7 @@ export default class WalletImport {
if (hd3.validateMnemonic()) {
await hd3.fetchBalance();
if (hd3.getBalance() > 0) {
await hd3.fetchTransactions();
// await hd3.fetchTransactions(); // experiment: dont fetch tx now. it will import faster. user can refresh his wallet later
return WalletImport._saveWallet(hd3);
}
}
@ -230,7 +230,7 @@ export default class WalletImport {
let watchOnly = new WatchOnlyWallet();
watchOnly.setSecret(importText);
if (watchOnly.valid()) {
await watchOnly.fetchTransactions();
// await watchOnly.fetchTransactions(); // experiment: dont fetch tx now. it will import faster. user can refresh his wallet later
await watchOnly.fetchBalance();
return WalletImport._saveWallet(watchOnly, additionalProperties);
}

2
ios/BlueWallet/Info.plist

@ -48,7 +48,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>5.0.1</string>
<string>5.1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>

2
ios/BlueWalletWatch Extension/Info.plist

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>5.0.1</string>
<string>5.1.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>5.0.1</string>
<string>5.1.0</string>
<key>CFBundleVersion</key>
<string>239</string>
<key>UISupportedInterfaceOrientations</key>

60
ios/Podfile.lock

@ -68,25 +68,23 @@ PODS:
- React
- react-native-blur (0.8.0):
- React
- react-native-camera (3.4.0):
- react-native-camera (3.17.0):
- React
- react-native-camera/RCT (= 3.4.0)
- react-native-camera/RN (= 3.4.0)
- react-native-camera/RCT (3.4.0):
- react-native-camera/RCT (= 3.17.0)
- react-native-camera/RN (= 3.17.0)
- react-native-camera/RCT (3.17.0):
- React
- react-native-camera/RN (3.4.0):
- react-native-camera/RN (3.17.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):
- React
- react-native-randombytes (3.5.3):
- React
- react-native-slider (2.0.8):
- React
- react-native-webview (6.9.0):
- react-native-webview (6.11.1):
- React
- React-RCTActionSheet (0.60.5):
- React-Core (= 0.60.5)
@ -115,30 +113,32 @@ PODS:
- React
- RemobileReactNativeQrcodeLocalImage (1.0.4):
- React
- RNCAsyncStorage (1.6.2):
- RNCAsyncStorage (1.7.1):
- React
- RNDefaultPreference (1.4.1):
- React
- RNDeviceInfo (4.0.1):
- React
- RNFS (2.13.3):
- RNFS (2.16.4):
- React
- RNGestureHandler (1.3.0):
- RNGestureHandler (1.5.6):
- React
- RNHandoff (0.0.3):
- React
- RNQuickAction (0.3.13):
- React
- RNRate (1.0.1):
- RNRate (1.1.10):
- React
- RNReactNativeHapticFeedback (1.9.0):
- React
- RNSecureKeyStore (1.0.0):
- React
- RNSentry (1.2.1):
- RNSentry (1.3.1):
- React
- Sentry (~> 4.4.0)
- RNShare (2.0.0):
- React
- RNSVG (9.5.1):
- RNSVG (9.13.6):
- React
- RNVectorIcons (6.6.0):
- React
@ -172,7 +172,6 @@ DEPENDENCIES:
- "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`)
- "react-native-slider (from `../node_modules/@react-native-community/slider`)"
@ -196,7 +195,8 @@ DEPENDENCIES:
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
- RNHandoff (from `../node_modules/react-native-handoff`)
- RNQuickAction (from `../node_modules/react-native-quick-actions`)
- RNRate (from `../node_modules/react-native-rate/ios`)
- RNRate (from `../node_modules/react-native-rate`)
- RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`)
- RNSecureKeyStore (from `../node_modules/react-native-secure-key-store/ios`)
- "RNSentry (from `../node_modules/@sentry/react-native`)"
- RNShare (from `../node_modules/react-native-share`)
@ -208,7 +208,7 @@ DEPENDENCIES:
- yoga (from `../node_modules/react-native/ReactCommon/yoga`)
SPEC REPOS:
https://github.com/cocoapods/specs.git:
trunk:
- boost-for-react-native
- EFQRCode
- lottie-ios
@ -248,8 +248,6 @@ EXTERNAL SOURCES:
: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:
:path: "../node_modules/react-native-image-picker"
react-native-randombytes:
@ -297,7 +295,9 @@ EXTERNAL SOURCES:
RNQuickAction:
:path: "../node_modules/react-native-quick-actions"
RNRate:
:path: "../node_modules/react-native-rate/ios"
:path: "../node_modules/react-native-rate"
RNReactNativeHapticFeedback:
:path: "../node_modules/react-native-haptic-feedback"
RNSecureKeyStore:
:path: "../node_modules/react-native-secure-key-store/ios"
RNSentry:
@ -335,13 +335,12 @@ SPEC CHECKSUMS:
React-jsinspector: e08662d1bf5b129a3d556eb9ea343a3f40353ae4
react-native-biometrics: c892904948a32295b128f633bcc11eda020645c5
react-native-blur: cad4d93b364f91e7b7931b3fa935455487e5c33c
react-native-camera: 203091b4bf99d48b788a0682ad573e8718724893
react-native-camera: 4ead7a30a89f275f531d80aa720cc69363c38135
react-native-document-picker: e3516aff0dcf65ee0785d9bcf190eb10e2261154
react-native-haptic-feedback: 22c9dc85fd8059f83bf9edd9212ac4bd4ae6074d
react-native-image-picker: 3637d63fef7e32a230141ab4660d3ceb773c824f
react-native-randombytes: 991545e6eaaf700b4ee384c291ef3d572e0b2ca8
react-native-slider: b2f361499888302147205f17f6fffa921a7bda70
react-native-webview: f72ac4078e115dfa741cc588acb1cca25566457d
react-native-webview: f11ac6c8bcaba5b71ddda1c12a10c8ea059b080f
React-RCTActionSheet: b0f1ea83f4bf75fb966eae9bfc47b78c8d3efd90
React-RCTAnimation: 359ba1b5690b1e87cc173558a78e82d35919333e
React-RCTBlob: 5e2b55f76e9a1c7ae52b826923502ddc3238df24
@ -354,18 +353,19 @@ SPEC CHECKSUMS:
React-RCTWebSocket: cd932a16b7214898b6b7f788c8bddb3637246ac4
ReactNativePrivacySnapshot: cc295e45dc22810e9ff2c93380d643de20a77015
RemobileReactNativeQrcodeLocalImage: 57aadc12896b148fb5e04bc7c6805f3565f5c3fa
RNCAsyncStorage: 5ae4d57458804e99f73d427214442a6b10a53856
RNCAsyncStorage: 8539fc80a0075fcc9c8e2dff84cd22dc5bf1dacf
RNDefaultPreference: 12d246dd2222e66dadcd76cc1250560663befc3a
RNDeviceInfo: 12faae605ba42a1a5041c3c41a77834bc23f049d
RNFS: c9bbde46b0d59619f8e7b735991c60e0f73d22c1
RNGestureHandler: 5329a942fce3d41c68b84c2c2276ce06a696d8b0
RNFS: 90d1a32d3bc8f75cc7fc3dd2f67506049664346b
RNGestureHandler: 911d3b110a7a233a34c4f800e7188a84b75319c6
RNHandoff: d3b0754cca3a6bcd9b25f544f733f7f033ccf5fa
RNQuickAction: 6d404a869dc872cde841ad3147416a670d13fa93
RNRate: 29be49c24b314c4e8ec09d848c3965f61cb0be47
RNRate: d44a8bca6ee08f5d890ecccddaec2810955ffbb3
RNReactNativeHapticFeedback: 2566b468cc8d0e7bb2f84b23adc0f4614594d071
RNSecureKeyStore: f1ad870e53806453039f650720d2845c678d89c8
RNSentry: 9b1d983b2d5d1c215ba6490348fd2a4cc23a8a9d
RNSentry: 6458ba85aa3f8ae291abed4f72abbd7080839c71
RNShare: 8b171d4b43c1d886917fdd303bf7a4b87167b05c
RNSVG: 0eb087cfb5d7937be93c45b163b26352a647e681
RNSVG: 8ba35cbeb385a52fd960fd28db9d7d18b4c2974f
RNVectorIcons: 0bb4def82230be1333ddaeee9fcba45f0b288ed4
RNWatch: a36ea17fac675b98b1d8cd41604af68cf1fa9a03
Sentry: 14bdd673870e8cf64932b149fad5bbbf39a9b390
@ -376,4 +376,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: f93db402b02d7f01a8bc51d9b2cc10a39391b081
COCOAPODS: 1.7.5
COCOAPODS: 1.8.4

2
ios/TodayExtension/Info.plist

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

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

@ -1,3 +1,18 @@
v5.1.0
======
* FIX: weird import screen scan qr code behaviour
* FIX: allow using unconfirmed utxo when creating transaction
* REF: removed obsolete single address RBF;
* REF: refactored breadwallet format a bit
* FIX: Wallet name input character entry bug
* REF: experimental - dont fetch transactions when importing wallet, only balance. should be faster. txs can be fetched later manually
* FIX: Import ColdCard wallet using JSON's label.
* REF: German translations
* REF: now BIP44 works through electrum
* REF: single-address wallets now work through electrum
* REF: wallet export screen improvements
v5.0.0
======
@ -40,19 +55,4 @@ v4.9.2
* 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
======
* ADD: Native segwit (BIP84) are now default wallets
* ADD: Toggle to turn off RBF when creating a transaction
* ADD: Scroll to end of wallets list when adding a wallet
* FIX: Default LN invoice expiry is now 24h instead of 1h
* FIX: Speeded up lighnint wallets
* FIX: Force Light theme mode even if system is in dark mode
* 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
* REF: Custom receive

42
loc/de_DE.js

@ -25,8 +25,8 @@ module.exports = {
empty_txs1: 'Deine Transaktionen erscheinen hier',
empty_txs2: 'Noch keine Transaktionen',
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.',
'Verwende das Lightning Wallet für Deine täglichen Bezahlungen. Lightning Transaktionen sind konkurrenzlos günstig und verblüffend schnell.',
empty_txs2_lightning: '\nDrücke zum Starten «Beträge verwalten», um das Wallet zu laden.',
tap_here_to_buy: 'Klicke hier, um Bitcoin zu kaufen',
},
reorder: {
@ -52,7 +52,7 @@ module.exports = {
details: {
title: 'Wallet',
address: 'Adresse',
master_fingerprint: 'Master fingerprint',
master_fingerprint: 'Fingerabdruckerkennung',
type: 'Typ',
label: 'Bezeichnung',
destination: 'Zieladresse',
@ -105,7 +105,7 @@ module.exports = {
tabBarLabel: 'Transaktionen',
title: 'Transaktionen',
description: 'Eine Liste eingehender oder ausgehender Transaktionen deiner Wallets',
conf: 'conf',
conf: 'Konf',
},
details: {
title: 'Transaktionen',
@ -130,7 +130,7 @@ module.exports = {
fee_placeholder: 'plus Gebühr (in BTC)',
note_placeholder: 'Notiz',
cancel: 'Abbrechen',
scan: 'Scan',
scan: 'Scannen',
send: 'Senden',
create: 'Erstellen',
remaining_balance: 'Verfügbarer Betrag',
@ -165,10 +165,10 @@ module.exports = {
share: 'Teilen',
copiedToClipboard: 'In die Zwischenablage kopiert.',
label: 'Beschreibung',
create: 'Create',
create: 'Erstelle',
setAmount: 'Zu erhaltender Betrag',
},
scan_lnurl: 'Scan to receive',
scan_lnurl: 'Scannen, zum Erhalten',
},
buyBitcoin: {
header: 'Kaufe Bitcoin',
@ -190,14 +190,14 @@ module.exports = {
'Bitte installier Lndhub, um mit deiner eigenen LND Node zu verbinden' +
' und setz seine URL hier in den Einstellungen. Lass das Feld leer, um Standard- ' +
'LndHub\n (lndhub.io) zu verwenden',
electrum_settings: 'Electrum Settings',
electrum_settings_explain: 'Set to blank to use default',
electrum_settings: 'Electrum Einstellungen',
electrum_settings_explain: 'Leer lassen, um den Standard zu verwenden.',
save: 'Speichern',
about: 'Über',
language: 'Sprache',
currency: 'Währung',
advanced_options: 'Advanced Options',
enable_advanced_mode: 'Enable advanced mode',
advanced_options: 'Erweiterte Optionen',
enable_advanced_mode: 'Erweiterter Modus verwenden',
},
plausibledeniability: {
title: 'Glaubhafte Täuschung',
@ -226,24 +226,24 @@ module.exports = {
refill_lnd_balance: 'Lade deine Lightning Wallet auf',
refill: 'Aufladen',
withdraw: 'Abheben',
expired: 'Expired',
placeholder: 'Invoice',
expired: 'Abgelaufen',
placeholder: 'Rechnung',
sameWalletAsInvoiceError:
'Du kannst nicht die Rechnung mit der Wallet begleichen, die du für die Erstellung dieser Rechnung verwendet hast.',
},
pleasebackup: {
title: 'Your wallet is created...',
title: 'Ihr Wallet wird erstellt...',
text:
"Please take a moment to write down this mnemonic phrase on a piece of paper. It's your backup you can use to restore the wallet on other device.",
ok: 'OK, I wrote this down!',
'Nimm Dir Zeit die mnemonischen Wörter zur späteren Wiederherstellung des Wallets aufzuschreiben. Die Wörter sind dien einziges Backup im Fall eines Geräteverlustes.',
ok: 'Ja, mein Geld ist sicher!',
},
lndViewInvoice: {
wasnt_paid_and_expired: 'This invoice was not paid for and has expired',
has_been_paid: 'This invoice has been paid for',
please_pay: 'Please pay',
wasnt_paid_and_expired: 'Diese Rechnung ist unbezahlt und abgelaufen.',
has_been_paid: 'Diese Rechnung wurde bezahlt.',
please_pay: 'Bitte zahle',
sats: 'sats',
for: 'For:',
for: 'r:',
additional_info: 'Additional Information',
open_direct_channel: 'Open direct channel with this node:',
open_direct_channel: 'Direkten Kanal zu diesem Knoten eröffnen:',
},
};

2292
package-lock.json

File diff suppressed because it is too large

44
package.json

@ -1,6 +1,6 @@
{
"name": "BlueWallet",
"version": "5.0.1",
"version": "5.1.0",
"devDependencies": {
"@babel/core": "^7.5.0",
"@babel/runtime": "^7.5.1",
@ -52,31 +52,31 @@
]
},
"dependencies": {
"@babel/preset-env": "7.5.0",
"@react-native-community/async-storage": "1.6.2",
"@babel/preset-env": "7.8.4",
"@react-native-community/async-storage": "1.7.1",
"@react-native-community/blur": "3.4.1",
"@react-native-community/slider": "2.0.8",
"@remobile/react-native-qrcode-local-image": "git+https://github.com/BlueWallet/react-native-qrcode-local-image.git",
"@sentry/react-native": "1.2.1",
"amplitude-js": "5.6.0",
"@sentry/react-native": "1.3.1",
"amplitude-js": "5.9.0",
"bech32": "1.1.3",
"bignumber.js": "9.0.0",
"bip21": "2.0.2",
"bip32": "2.0.3",
"bip32": "2.0.5",
"bip39": "2.5.0",
"bitcoinjs-lib": "5.1.6",
"bolt11": "1.2.7",
"buffer": "5.2.1",
"buffer": "5.4.3",
"buffer-reverse": "1.0.1",
"coinselect": "3.1.11",
"crypto-js": "3.1.9-1",
"dayjs": "1.8.14",
"dayjs": "1.8.20",
"ecurve": "1.0.6",
"electrum-client": "git+https://github.com/BlueWallet/rn-electrum-client.git",
"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",
"eslint-plugin-prettier": "3.1.2",
"eslint-plugin-standard": "4.0.0",
"events": "1.1.1",
"frisbee": "2.0.9",
@ -85,50 +85,50 @@
"mocha": "5.2.0",
"node-libs-react-native": "1.0.3",
"path-browserify": "1.0.0",
"prettier": "1.18.2",
"prettier": "1.19.1",
"process": "0.11.10",
"prop-types": "15.7.2",
"react": "16.8.6",
"react-localization": "1.0.13",
"react-localization": "1.0.15",
"react-native": "0.60.5",
"react-native-biometrics": "git+https://github.com/BlueWallet/react-native-biometrics.git#2.0.0",
"react-native-camera": "3.4.0",
"react-native-camera": "3.17.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",
"react-native-gesture-handler": "1.3.0",
"react-native-fs": "2.16.4",
"react-native-gesture-handler": "1.5.6",
"react-native-handoff": "git+https://github.com/marcosrdz/react-native-handoff.git",
"react-native-haptic-feedback": "1.7.1",
"react-native-haptic-feedback": "1.9.0",
"react-native-image-picker": "1.1.0",
"react-native-level-fs": "3.0.1",
"react-native-linear-gradient": "2.5.4",
"react-native-modal": "11.1.0",
"react-native-linear-gradient": "2.5.6",
"react-native-modal": "11.5.3",
"react-native-obscure": "1.2.1",
"react-native-popup-menu-android": "1.0.3",
"react-native-privacy-snapshot": "git+https://github.com/BlueWallet/react-native-privacy-snapshot.git",
"react-native-prompt-android": "git+https://github.com/marcosrdz/react-native-prompt-android.git",
"react-native-qrcode-svg": "5.1.2",
"react-native-qrcode-svg": "5.3.2",
"react-native-quick-actions": "0.3.13",
"react-native-randombytes": "3.5.3",
"react-native-rate": "1.1.7",
"react-native-rate": "1.1.10",
"react-native-secure-key-store": "git+https://github.com/marcosrdz/react-native-secure-key-store.git#38332f629f577cdd57c69fc8cc971b3cbad193c9",
"react-native-share": "2.0.0",
"react-native-snap-carousel": "3.8.4",
"react-native-sortable-list": "0.0.23",
"react-native-svg": "9.5.1",
"react-native-svg": "9.13.6",
"react-native-swiper": "git+https://github.com/BlueWallet/react-native-swiper.git#1.5.14",
"react-native-tcp": "git+https://github.com/aprock/react-native-tcp.git",
"react-native-tooltip": "git+https://github.com/marcosrdz/react-native-tooltip.git",
"react-native-vector-icons": "6.6.0",
"react-native-watch-connectivity": "0.4.2",
"react-native-webview": "6.9.0",
"react-native-webview": "6.11.1",
"react-navigation": "3.11.0",
"react-navigation-hooks": "1.1.0",
"react-test-render": "1.1.2",
"readable-stream": "3.4.0",
"readable-stream": "3.6.0",
"secure-random": "1.1.2",
"stream-browserify": "2.0.2",
"url": "0.11.0",

5
screen/lnd/lndCreateInvoice.js

@ -79,8 +79,7 @@ export default class LNDCreateInvoice extends Component {
onFailure: () => {
this.props.navigation.dismiss();
this.props.navigation.navigate('WalletExport', {
address: this.state.fromWallet.getAddress(),
secret: this.state.fromWallet.getSecret(),
wallet: this.state.fromWallet,
});
},
});
@ -210,7 +209,7 @@ export default class LNDCreateInvoice extends Component {
<TouchableOpacity
disabled={this.state.isLoading}
onPress={() => {
NavigationService.navigate('ScanQrAddress', {
NavigationService.navigate('ScanQRCode', {
onBarScanned: this.processLnurl,
launchedBy: this.props.navigation.state.routeName,
});

3
screen/receive/details.js

@ -109,8 +109,7 @@ export default class ReceiveDetails extends Component {
onFailure: () => {
this.props.navigation.goBack();
this.props.navigation.navigate('WalletExport', {
address: this.wallet.getAddress(),
secret: this.wallet.getSecret(),
wallet: this.wallet,
});
},
});

19
screen/send/scanQrAddress.js → screen/send/ScanQRCode.js

@ -9,6 +9,7 @@ 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 createHash = require('create-hash');
const ScanQRCode = ({
onBarScanned = useNavigationParam('onBarScanned'),
@ -21,7 +22,23 @@ const ScanQRCode = ({
const [isLoading, setIsLoading] = useState(false);
const { navigate, goBack } = useNavigation();
const scannedCache = {};
const HashIt = function(s) {
return createHash('sha256')
.update(s)
.digest()
.toString('hex');
};
const onBarCodeRead = ret => {
const h = HashIt(ret.data);
if (scannedCache[h]) {
// this QR was already scanned by this ScanQRCode, lets prevent firing duplicate callbacks
return;
}
scannedCache[h] = +new Date();
if (!isLoading && !cameraPreviewIsPaused) {
setIsLoading(true);
try {
@ -51,7 +68,7 @@ const ScanQRCode = ({
if (fileParsed.keystore.ckcc_xfp) {
masterFingerprint = Number(fileParsed.keystore.ckcc_xfp);
}
onBarCodeRead({ data: fileParsed.keystore.xpub, additionalProperties: { masterFingerprint } });
onBarCodeRead({ data: fileParsed.keystore.xpub, additionalProperties: { masterFingerprint, label: fileParsed.keystore.label } });
} else {
throw new Error();
}

8
screen/settings/about.js

@ -121,12 +121,10 @@ const About = () => {
<BlueText h3>Built with awesome:</BlueText>
<BlueSpacing20 />
<BlueText h4>* React Native</BlueText>
<BlueText h4>* Bitcoinjs-lib</BlueText>
<BlueText h4>* blockcypher.com API</BlueText>
<BlueText h4>* bitcoinjs-lib</BlueText>
<BlueText h4>* Nodejs</BlueText>
<BlueText h4>* react-native-elements</BlueText>
<BlueText h4>* rn-nodeify</BlueText>
<BlueText h4>* bignumber.js</BlueText>
<BlueText h4>* Electrum server</BlueText>
<BlueSpacing20 />
<BlueSpacing20 />
<BlueButton onPress={handleOnReleaseNotesPress} title="Release notes" />

255
screen/transactions/RBF-create.js

@ -1,255 +0,0 @@
/** @type {AppStorage} */
/* global alert */
import React, { Component } from 'react';
import { TextInput, View, ActivtyIndicator } from 'react-native';
import { FormValidationMessage } from 'react-native-elements';
import {
BlueLoading,
BlueSpacing20,
BlueButton,
SafeBlueArea,
BlueCard,
BlueText,
BlueSpacing,
BlueNavigationStyle,
} from '../../BlueComponents';
import PropTypes from 'prop-types';
const bitcoinjs = require('bitcoinjs-lib');
let BigNumber = require('bignumber.js');
let BlueApp = require('../../BlueApp');
export default class SendCreate extends Component {
static navigationOptions = () => ({
...BlueNavigationStyle(null, false),
title: 'Create RBF',
});
constructor(props) {
super(props);
console.log('send/create constructor');
if (!props.navigation.state.params.feeDelta) {
props.navigation.state.params.feeDelta = '0';
}
this.state = {
isLoading: true,
feeDelta: props.navigation.state.params.feeDelta,
newDestinationAddress: props.navigation.state.params.newDestinationAddress,
txid: props.navigation.state.params.txid,
sourceTx: props.navigation.state.params.sourceTx,
fromWallet: props.navigation.state.params.sourceWallet,
};
}
async componentDidMount() {
console.log('RBF-create - componentDidMount');
let utxo = [];
let lastSequence = 0;
let totalInputAmountSatoshi = 0;
for (let input of this.state.sourceTx.inputs) {
if (input.sequence > lastSequence) {
lastSequence = input.sequence;
}
totalInputAmountSatoshi += input.output_value;
// let amount = new BigNumber(input.output_value)
// amount = amount.div(10000000).toString(10)
utxo.push({
tx_hash: input.prev_hash,
tx_output_n: input.output_index,
value: input.output_value,
});
}
// check seq=MAX and fail if it is
if (lastSequence === bitcoinjs.Transaction.DEFAULT_SEQUENCE) {
return this.setState({
isLoading: false,
nonReplaceable: true,
});
// lastSequence = 1
}
let txMetadata = BlueApp.tx_metadata[this.state.txid];
if (txMetadata) {
if (txMetadata.last_sequence) {
lastSequence = Math.max(lastSequence, txMetadata.last_sequence);
}
}
lastSequence += 1;
let changeAddress;
let transferAmount;
let totalOutputAmountSatoshi = 0;
for (let o of this.state.sourceTx.outputs) {
totalOutputAmountSatoshi += o.value;
if (o.addresses[0] === this.state.fromWallet.getAddress()) {
// change
changeAddress = o.addresses[0];
} else {
transferAmount = new BigNumber(o.value);
transferAmount = transferAmount.dividedBy(100000000).toString(10);
}
}
let oldFee = new BigNumber(totalInputAmountSatoshi - totalOutputAmountSatoshi);
oldFee = parseFloat(oldFee.dividedBy(100000000).toString(10));
console.log('changeAddress = ', changeAddress);
console.log('utxo', utxo);
console.log('lastSequence', lastSequence);
console.log('totalInputAmountSatoshi', totalInputAmountSatoshi);
console.log('totalOutputAmountSatoshi', totalOutputAmountSatoshi);
console.log('transferAmount', transferAmount);
console.log('oldFee', oldFee);
let newFee = new BigNumber(oldFee);
newFee = newFee.plus(this.state.feeDelta).toString(10);
console.log('new Fee', newFee);
// creating TX
setTimeout(() => {
// more responsive
let tx;
try {
tx = this.state.fromWallet.createTx(utxo, transferAmount, newFee, this.state.newDestinationAddress, false, lastSequence);
BlueApp.tx_metadata[this.state.txid] = txMetadata || {};
BlueApp.tx_metadata[this.state.txid]['last_sequence'] = lastSequence;
// in case new TX get confirmed, we must save metadata under new txid
let bitcoin = bitcoinjs;
let txDecoded = bitcoin.Transaction.fromHex(tx);
let txid = txDecoded.getId();
BlueApp.tx_metadata[txid] = BlueApp.tx_metadata[this.state.txid];
BlueApp.tx_metadata[txid]['txhex'] = tx;
//
BlueApp.saveToDisk();
console.log('BlueApp.txMetadata[this.state.txid]', BlueApp.tx_metadata[this.state.txid]);
} catch (err) {
console.log(err);
return this.setState({
isError: true,
errorMessage: JSON.stringify(err.message),
});
}
let newFeeSatoshi = new BigNumber(newFee);
newFeeSatoshi = parseInt(newFeeSatoshi.multipliedBy(100000000));
let satoshiPerByte = Math.round(newFeeSatoshi / (tx.length / 2));
this.setState({
isLoading: false,
size: Math.round(tx.length / 2),
tx,
satoshiPerByte: satoshiPerByte,
amount: transferAmount,
fee: newFee,
});
}, 10);
}
async broadcast() {
this.setState({ isLoading: true }, async () => {
console.log('broadcasting', this.state.tx);
let result = await this.state.fromWallet.broadcastTx(this.state.tx);
console.log('broadcast result = ', result);
if (typeof result === 'string') {
try {
result = JSON.parse(result);
} catch (_) {
result = { result };
}
}
if (result && result.error) {
alert(JSON.stringify(result.error));
this.setState({ isLoading: false });
} else {
alert(JSON.stringify(result.result || result.txid));
this.props.navigation.navigate('TransactionStatus');
}
});
}
render() {
if (this.state.isError) {
return (
<SafeBlueArea style={{ flex: 1, paddingTop: 20 }}>
<BlueSpacing />
<BlueCard title={'Replace Transaction'} style={{ alignItems: 'center', flex: 1 }}>
<BlueText>Error creating transaction. Invalid address or send amount?</BlueText>
<FormValidationMessage>{this.state.errorMessage}</FormValidationMessage>
</BlueCard>
<BlueButton onPress={() => this.props.navigation.goBack()} title="Go back" />
</SafeBlueArea>
);
}
if (this.state.isLoading) {
return <BlueLoading />;
}
if (this.state.nonReplaceable) {
return (
<SafeBlueArea style={{ flex: 1, paddingTop: 20 }}>
<View style={{ flex: 1, justifyContent: 'center', alignContent: 'center' }}>
<BlueText h4 style={{ textAlign: 'center' }}>
This transaction is not replaceable
</BlueText>
</View>
</SafeBlueArea>
);
}
return (
<SafeBlueArea style={{ flex: 1, paddingTop: 20 }}>
<BlueSpacing />
<BlueCard title={'Replace Transaction'} style={{ alignItems: 'center', flex: 1 }}>
<BlueText>This is your transaction's hex, signed and ready to be broadcasted to the network. Continue?</BlueText>
<TextInput
style={{
borderColor: '#ebebeb',
borderWidth: 1,
marginTop: 20,
color: '#ebebeb',
}}
maxHeight={70}
multiline
editable={false}
value={this.state.tx}
/>
<BlueSpacing20 />
<BlueText style={{ paddingTop: 20 }}>To: {this.state.newDestinationAddress}</BlueText>
<BlueText>Amount: {this.state.amount} BTC</BlueText>
<BlueText>Fee: {this.state.fee} BTC</BlueText>
<BlueText>TX size: {this.state.size} Bytes</BlueText>
<BlueText>satoshiPerByte: {this.state.satoshiPerByte} Sat/B</BlueText>
</BlueCard>
{this.state.isLoading ? (
<ActivtyIndicator />
) : (
<BlueButton icon={{ name: 'bullhorn', type: 'font-awesome' }} onPress={() => this.broadcast()} title="Broadcast" />
)}
</SafeBlueArea>
);
}
}
SendCreate.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.func,
navigate: PropTypes.func,
state: PropTypes.shape({
params: PropTypes.shape({
address: PropTypes.string,
feeDelta: PropTypes.string,
fromAddress: PropTypes.string,
newDestinationAddress: PropTypes.string,
txid: PropTypes.string,
sourceTx: PropTypes.object,
sourceWallet: PropTypes.object,
}),
}),
}),
};

202
screen/transactions/RBF.js

@ -1,202 +0,0 @@
import React, { Component } from 'react';
import { ActivityIndicator, View, TextInput } from 'react-native';
import { BlueSpacing20, BlueButton, SafeBlueArea, BlueCard, BlueText, BlueSpacing, BlueNavigationStyle } from '../../BlueComponents';
import PropTypes from 'prop-types';
import { SegwitBech32Wallet } from '../../class';
/** @type {AppStorage} */
let BlueApp = require('../../BlueApp');
export default class RBF extends Component {
static navigationOptions = () => ({
...BlueNavigationStyle(null, false),
title: 'RBF',
});
constructor(props) {
super(props);
let txid;
if (props.navigation.state.params) txid = props.navigation.state.params.txid;
let sourceWallet;
let sourceTx;
for (let w of BlueApp.getWallets()) {
for (let t of w.getTransactions()) {
if (t.hash === txid) {
// found our source wallet
sourceWallet = w;
sourceTx = t;
console.log(t);
}
}
}
let destinationAddress;
for (let o of sourceTx.outputs) {
if (!o.addresses && o.script) {
// probably bech32 output, so we need to decode address
o.addresses = [SegwitBech32Wallet.scriptPubKeyToAddress(o.script)];
}
if (o.addresses && o.addresses[0] === sourceWallet.getAddress()) {
// change
// nop
} else {
// DESTINATION address
destinationAddress = (o.addresses && o.addresses[0]) || '';
console.log('dest = ', destinationAddress);
}
}
if (!destinationAddress || sourceWallet.type === 'legacy') {
// for now I'm too lazy to add RBF support for legacy addresses
this.state = {
isLoading: false,
nonReplaceable: true,
};
return;
}
this.state = {
isLoading: true,
txid,
sourceTx,
sourceWallet,
newDestinationAddress: destinationAddress,
feeDelta: '',
};
}
async componentDidMount() {
let startTime = Date.now();
console.log('transactions/RBF - componentDidMount');
this.setState({
isLoading: false,
});
let endTime = Date.now();
console.log('componentDidMount took', (endTime - startTime) / 1000, 'sec');
}
createTransaction() {
this.props.navigation.navigate('CreateRBF', {
feeDelta: this.state.feeDelta,
newDestinationAddress: this.state.newDestinationAddress,
txid: this.state.txid,
sourceTx: this.state.sourceTx,
sourceWallet: this.state.sourceWallet,
});
}
render() {
if (this.state.isLoading) {
return (
<View style={{ flex: 1, paddingTop: 20 }}>
<ActivityIndicator />
</View>
);
}
if (this.state.nonReplaceable) {
return (
<SafeBlueArea style={{ flex: 1, paddingTop: 20 }}>
<BlueSpacing20 />
<BlueSpacing20 />
<BlueSpacing20 />
<BlueSpacing20 />
<BlueSpacing20 />
<BlueText h4>This transaction is not replaceable</BlueText>
<BlueButton onPress={() => this.props.navigation.goBack()} title="Back" />
</SafeBlueArea>
);
}
if (!this.state.sourceWallet.getAddress) {
return (
<SafeBlueArea style={{ flex: 1, paddingTop: 20 }}>
<BlueText>System error: Source wallet not found (this should never happen)</BlueText>
<BlueButton onPress={() => this.props.navigation.goBack()} title="Back" />
</SafeBlueArea>
);
}
return (
<SafeBlueArea style={{ flex: 1, paddingTop: 20 }}>
<BlueSpacing />
<BlueCard title={'Replace By Fee'} style={{ alignItems: 'center', flex: 1 }}>
<BlueText>RBF allows you to increase fee on already sent but not confirmed transaction, thus speeding up mining</BlueText>
<BlueSpacing20 />
<BlueText>
From wallet '{this.state.sourceWallet.getLabel()}' ({this.state.sourceWallet.getAddress()})
</BlueText>
<BlueSpacing20 />
<View
style={{
flexDirection: 'row',
borderColor: '#d2d2d2',
borderBottomColor: '#d2d2d2',
borderWidth: 1.0,
borderBottomWidth: 0.5,
backgroundColor: '#f5f5f5',
minHeight: 44,
height: 44,
alignItems: 'center',
marginVertical: 8,
borderRadius: 4,
}}
>
<TextInput
onChangeText={text => this.setState({ newDestinationAddress: text })}
placeholder={'receiver address here'}
value={this.state.newDestinationAddress}
style={{ flex: 1, minHeight: 33, marginHorizontal: 8 }}
/>
</View>
<View
style={{
flexDirection: 'row',
borderColor: '#d2d2d2',
borderBottomColor: '#d2d2d2',
borderWidth: 1.0,
borderBottomWidth: 0.5,
backgroundColor: '#f5f5f5',
minHeight: 44,
height: 44,
alignItems: 'center',
marginVertical: 8,
borderRadius: 4,
}}
>
<TextInput
onChangeText={text => this.setState({ feeDelta: text })}
keyboardType={'numeric'}
placeholder={'fee to add (in BTC)'}
value={this.state.feeDelta + ''}
style={{ flex: 1, minHeight: 33, marginHorizontal: 8 }}
/>
</View>
</BlueCard>
<BlueSpacing />
<BlueButton onPress={() => this.createTransaction()} title="Create" />
</SafeBlueArea>
);
}
}
RBF.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.func,
navigate: PropTypes.func,
state: PropTypes.shape({
params: PropTypes.shape({
txid: PropTypes.string,
}),
}),
}),
};

26
screen/transactions/transactionStatus.js

@ -13,7 +13,7 @@ import {
BlueNavigationStyle,
} from '../../BlueComponents';
import PropTypes from 'prop-types';
import { HDSegwitBech32Transaction, HDSegwitBech32Wallet } from '../../class';
import { HDSegwitBech32Transaction } from '../../class';
import { BitcoinUnit } from '../../models/bitcoinUnits';
import { Icon } from 'react-native-elements';
import Handoff from 'react-native-handoff';
@ -93,7 +93,7 @@ export default class TransactionsStatus extends Component {
}
async checkPossibilityOfCPFP() {
if (this.state.wallet.type !== HDSegwitBech32Wallet.type) {
if (!this.state.wallet.allowRBF()) {
return this.setState({ isCPFPpossible: buttonStatus.notPossible });
}
@ -106,7 +106,7 @@ export default class TransactionsStatus extends Component {
}
async checkPossibilityOfRBFBumpFee() {
if (this.state.wallet.type !== HDSegwitBech32Wallet.type) {
if (!this.state.wallet.allowRBF()) {
return this.setState({ isRBFBumpFeePossible: buttonStatus.notPossible });
}
@ -119,7 +119,7 @@ export default class TransactionsStatus extends Component {
}
async checkPossibilityOfRBFCancel() {
if (this.state.wallet.type !== HDSegwitBech32Wallet.type) {
if (!this.state.wallet.allowRBF()) {
return this.setState({ isRBFCancelPossible: buttonStatus.notPossible });
}
@ -250,24 +250,6 @@ export default class TransactionsStatus extends Component {
</BlueCard>
<View style={{ alignSelf: 'center', justifyContent: 'center' }}>
{(() => {
if (this.state.tx.confirmations === 0 && this.state.wallet && this.state.wallet.allowRBF()) {
return (
<React.Fragment>
<BlueButton
onPress={() =>
this.props.navigation.navigate('RBF', {
txid: this.state.tx.hash,
})
}
title="Replace-By-Fee (RBF)"
/>
<BlueSpacing20 />
</React.Fragment>
);
}
})()}
{(() => {
if (this.state.isCPFPpossible === buttonStatus.unknown) {
return (

57
screen/wallets/add.js

@ -98,36 +98,35 @@ export default class WalletsAdd extends Component {
<SafeBlueArea forceInset={{ horizontal: 'always' }} style={{ flex: 1, paddingTop: 40 }}>
<ScrollView>
<BlueFormLabel>{loc.wallets.add.wallet_name}</BlueFormLabel>
<KeyboardAvoidingView
style={{
flexDirection: 'row',
borderColor: '#d2d2d2',
borderBottomColor: '#d2d2d2',
borderWidth: 1.0,
borderBottomWidth: 0.5,
backgroundColor: '#f5f5f5',
minHeight: 44,
height: 44,
marginHorizontal: 20,
alignItems: 'center',
marginVertical: 16,
borderRadius: 4,
}}
enabled
behavior={Platform.OS === 'ios' ? 'position' : null}
keyboardVerticalOffset={20}
>
<TextInput
value={this.state.label}
placeholderTextColor="#81868e"
placeholder="my first wallet"
onChangeText={text => {
this.setLabel(text);
<KeyboardAvoidingView enabled behavior={Platform.OS === 'ios' ? 'position' : null} keyboardVerticalOffset={20}>
<View
style={{
flexDirection: 'row',
borderColor: '#d2d2d2',
borderBottomColor: '#d2d2d2',
borderWidth: 1.0,
borderBottomWidth: 0.5,
backgroundColor: '#f5f5f5',
minHeight: 44,
height: 44,
marginHorizontal: 20,
alignItems: 'center',
marginVertical: 16,
borderRadius: 4,
}}
style={{ flex: 1, marginHorizontal: 8, color: '#81868e' }}
editable={!this.state.isLoading}
underlineColorAndroid="transparent"
/>
>
<TextInput
value={this.state.label}
placeholderTextColor="#81868e"
placeholder="my first wallet"
onChangeText={text => {
this.setLabel(text);
}}
style={{ flex: 1, marginHorizontal: 8, color: '#81868e' }}
editable={!this.state.isLoading}
underlineColorAndroid="transparent"
/>
</View>
</KeyboardAvoidingView>
<BlueFormLabel>{loc.wallets.add.wallet_type}</BlueFormLabel>

3
screen/wallets/details.js

@ -218,8 +218,7 @@ export default class WalletDetails extends Component {
<BlueButton
onPress={() =>
this.props.navigation.navigate('WalletExport', {
address: this.state.wallet.getAddress(),
secret: this.state.wallet.getSecret(),
wallet: this.state.wallet,
})
}
title={loc.wallets.details.export_backup}

19
screen/wallets/export.js

@ -5,7 +5,7 @@ import { BlueSpacing20, SafeBlueArea, BlueNavigationStyle, BlueText, BlueCopyTex
import PropTypes from 'prop-types';
import Privacy from '../../Privacy';
import Biometric from '../../class/biometrics';
import { LightningCustodianWallet } from '../../class';
import { LegacyWallet, LightningCustodianWallet, SegwitBech32Wallet, SegwitP2SHWallet } from '../../class';
/** @type {AppStorage} */
let BlueApp = require('../../BlueApp');
let loc = require('../../loc');
@ -20,17 +20,7 @@ export default class WalletExport extends Component {
constructor(props) {
super(props);
let address = props.navigation.state.params.address;
let secret = props.navigation.state.params.secret;
let wallet;
for (let w of BlueApp.getWallets()) {
if ((address && w.getAddress() === address) || w.getSecret() === secret) {
// found our wallet
wallet = w;
}
}
let wallet = props.navigation.state.params.wallet;
this.state = {
isLoading: true,
qrCodeHeight: height > width ? width - 40 : width / 2,
@ -89,7 +79,7 @@ export default class WalletExport extends Component {
</View>
{(() => {
if (this.state.wallet.getAddress()) {
if ([LegacyWallet.type, SegwitBech32Wallet.type, SegwitP2SHWallet.type].includes(this.state.wallet.type)) {
return (
<BlueCard>
<BlueText>{this.state.wallet.getAddress()}</BlueText>
@ -125,8 +115,7 @@ WalletExport.propTypes = {
navigation: PropTypes.shape({
state: PropTypes.shape({
params: PropTypes.shape({
address: PropTypes.string,
secret: PropTypes.string,
wallet: PropTypes.object.isRequired,
}),
}),
navigate: PropTypes.func,

2
screen/wallets/import.js

@ -120,7 +120,7 @@ const WalletsImport = () => {
<BlueButtonLink
title={loc.wallets.import.scan_qr}
onPress={() => {
navigate('ScanQrAddress', { launchedBy: 'ImportWallet', onBarScanned, showFileImportButton: true });
navigate('ScanQRCode', { launchedBy: 'ImportWallet', onBarScanned, showFileImportButton: true });
}}
/>
</View>

2
screen/wallets/list.js

@ -19,7 +19,7 @@ import PropTypes from 'prop-types';
import { PlaceholderWallet } from '../../class';
import WalletImport from '../../class/walletImport';
import Swiper from 'react-native-swiper';
import ScanQRCode from '../send/scanQrAddress';
import ScanQRCode from '../send/ScanQRCode';
import DeeplinkSchemaMatch from '../../class/deeplinkSchemaMatch';
let EV = require('../../events');
let A = require('../../analytics');

3
screen/wallets/transactions.js

@ -466,8 +466,7 @@ export default class WalletTransactions extends Component {
},
onFailure: () =>
this.props.navigation.navigate('WalletExport', {
address: this.state.wallet.getAddress(),
secret: this.state.wallet.getSecret(),
wallet: this.state.wallet,
}),
});
}

110
tests/integration/App.test.js

@ -1,6 +1,6 @@
/* global describe, it, expect, jest, jasmine */
import React from 'react';
import { LegacyWallet, SegwitP2SHWallet, AppStorage } from '../../class';
import { AppStorage } from '../../class';
import TestRenderer from 'react-test-renderer';
import Settings from '../../screen/settings/settings';
import Selftest from '../../screen/selftest';
@ -49,28 +49,6 @@ jest.mock('ScrollView', () => {
return ScrollView;
});
describe('unit - LegacyWallet', function() {
it('serialize and unserialize work correctly', () => {
let a = new LegacyWallet();
a.setLabel('my1');
let key = JSON.stringify(a);
let b = LegacyWallet.fromJson(key);
assert(key === JSON.stringify(b));
assert.strictEqual(key, JSON.stringify(b));
});
it('can validate addresses', () => {
let w = new LegacyWallet();
assert.ok(w.isAddressValid('12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG'));
assert.ok(!w.isAddressValid('12eQ9m4sgAwTSQoNXkRABKhCXCsjm2j'));
assert.ok(w.isAddressValid('3BDsBDxDimYgNZzsqszNZobqQq3yeUoJf2'));
assert.ok(!w.isAddressValid('3BDsBDxDimYgNZzsqszNZobqQq3yeUo'));
assert.ok(!w.isAddressValid('12345'));
});
});
it('BlueHeader works', () => {
const rendered = TestRenderer.create(<BlueHeader />).toJSON();
expect(rendered).toBeTruthy();
@ -105,92 +83,6 @@ it('Selftest work', () => {
assert.ok(okFound, 'OK not found. Got: ' + allTests.join('; '));
});
it('Wallet can fetch UTXO', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000;
let w = new SegwitP2SHWallet();
w._address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX';
await w.fetchUtxo();
assert.ok(w.utxo.length > 0, 'unexpected empty UTXO');
});
it('SegwitP2SHWallet can generate segwit P2SH address from WIF', async () => {
let l = new SegwitP2SHWallet();
l.setSecret('Kxr9tQED9H44gCmp6HAdmemAzU3n84H3dGkuWTKvE23JgHMW8gct');
assert.ok(l.getAddress() === '34AgLJhwXrvmkZS1o5TrcdeevMt22Nar53', 'expected ' + l.getAddress());
assert.ok(l.getAddress() === (await l.getAddressAsync()));
});
it('Wallet can fetch balance', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000;
let w = new LegacyWallet();
w._address = '115fUy41sZkAG14CmdP1VbEKcNRZJWkUWG'; // hack internals
assert.ok(w.getBalance() === 0);
assert.ok(w.getUnconfirmedBalance() === 0);
assert.ok(w._lastBalanceFetch === 0);
await w.fetchBalance();
assert.ok(w.getBalance() === 18262000);
assert.ok(w.getUnconfirmedBalance() === 0);
assert.ok(w._lastBalanceFetch > 0);
});
it('Wallet can fetch TXs', async () => {
let w = new LegacyWallet();
w._address = '12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG';
await w.fetchTransactions();
assert.strictEqual(w.getTransactions().length, 2);
let tx0 = w.getTransactions()[0];
let txExpected = {
block_hash: '0000000000000000000d05c54a592db8532f134e12b4c3ae0821ce582fad3566',
block_height: 530933,
block_index: 1587,
hash: '4924f3a29acdee007ebcf6084d2c9e1752c4eb7f26f7d1a06ef808780bf5fe6d',
addresses: ['12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG', '3BDsBDxDimYgNZzsqszNZobqQq3yeUoJf2'],
total: 800,
fees: 200,
size: 190,
preference: 'low',
relayed_by: '18.197.135.148:8333',
confirmed: '2018-07-07T20:05:30Z',
received: '2018-07-07T20:02:01.637Z',
ver: 1,
double_spend: false,
vin_sz: 1,
vout_sz: 1,
confirmations: 593,
confidence: 1,
inputs: [
{
prev_hash: 'd0432027a86119c63a0be8fa453275c2333b59067f1e559389cd3e0e377c8b96',
output_index: 1,
script:
'483045022100e443784abe25b6d39e01c95900834bf4eeaa82505ac0eb84c08e11c287d467de02203327c2b1136f4976f755ed7631b427d66db2278414e7faf1268eedf44c034e0c012103c69b905f7242b3688122f06951339a1ee00da652f6ecc6527ea6632146cace62',
output_value: 1000,
sequence: 4294967295,
addresses: ['12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG'],
script_type: 'pay-to-pubkey-hash',
age: 530926,
},
],
outputs: [
{
value: 800,
script: 'a914688eb9af71aab8ca221f4e6171a45fc46ea8743b87',
spent_by: '009c6219deeac341833642193e4a3b72e511105a61b48e375c5025b1bcbd6fb5',
addresses: ['3BDsBDxDimYgNZzsqszNZobqQq3yeUoJf2'],
script_type: 'pay-to-script-hash',
},
],
value: -1000,
};
delete tx0.confirmations;
delete txExpected.confirmations;
delete tx0.preference; // that bs is not always the same
delete txExpected.preference;
assert.deepStrictEqual(tx0, txExpected);
});
describe('currency', () => {
it('fetches exchange rate and saves to AsyncStorage', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000;

30
tests/integration/HDWallet.test.js

@ -306,7 +306,7 @@ it('Legacy HD (BIP44) can generate addressess based on xpub', async function() {
assert.strictEqual(hd._getInternalAddressByIndex(1), '13CW9WWBsWpDUvLtbFqYziWBWTYUoQb4nU');
});
it.skip('Legacy HD (BIP44) can create TX', async () => {
it('Legacy HD (BIP44) can create TX', async () => {
if (!process.env.HD_MNEMONIC) {
console.error('process.env.HD_MNEMONIC not set, skipped');
return;
@ -315,15 +315,14 @@ it.skip('Legacy HD (BIP44) can create TX', async () => {
hd.setSecret(process.env.HD_MNEMONIC);
assert.ok(hd.validateMnemonic());
await hd.fetchBalance();
await hd.fetchUtxo();
assert.strictEqual(hd.utxo.length, 4);
await hd.getChangeAddressAsync(); // to refresh internal pointer to next free address
await hd.getAddressAsync(); // to refresh internal pointer to next free address
let txhex = hd.createTx(hd.utxo, 0.0008, 0.000005, '3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK');
assert.strictEqual(
txhex,
'01000000045fbc74110c2d6fcf4d1161a59913fbcd2b6ab3c5a9eb4d0dc0859515cbc8654f030000006b4830450221009be5dbe37db5a8409ddce3570140c95d162a07651b1e48cf39a6a741892adc53022061a25b8024d8f3cb1b94f264245de0c6e9a103ea557ddeb66245b40ec8e9384b012102ad7b2216f3a2b38d56db8a7ee5c540fd12c4bbb7013106eff78cc2ace65aa002ffffffff5fbc74110c2d6fcf4d1161a59913fbcd2b6ab3c5a9eb4d0dc0859515cbc8654f000000006a47304402207106e9fa4e2e35d351fbccc9c0fad3356d85d0cd35a9d7e9cbcefce5440da1e5022073c1905b5927447378c0f660e62900c1d4b2691730799458889fb87d86f5159101210316e84a2556f30a199541633f5dda6787710ccab26771b7084f4c9e1104f47667ffffffff5fbc74110c2d6fcf4d1161a59913fbcd2b6ab3c5a9eb4d0dc0859515cbc8654f020000006a4730440220250b15094096c4d4fe6793da8e45fa118ed057cc2759a480c115e76e23590791022079cdbdc9e630d713395602071e2837ecc1d192a36a24d8ec71bc51d5e62b203b01210316e84a2556f30a199541633f5dda6787710ccab26771b7084f4c9e1104f47667ffffffff5fbc74110c2d6fcf4d1161a59913fbcd2b6ab3c5a9eb4d0dc0859515cbc8654f010000006b483045022100879da610e6ed12c84d55f12baf3bf6222d59b5282502b3c7f4db1d22152c16900220759a1c88583cbdaf7fde21c273ad985dfdf94a2fa85e42ee41dcea2fd69136fd012102ad7b2216f3a2b38d56db8a7ee5c540fd12c4bbb7013106eff78cc2ace65aa002ffffffff02803801000000000017a914a3a65daca3064280ae072b9d6773c027b30abace872c4c0000000000001976a9146ee5e3e66dc73587a3a2d77a1a6c8554fae21b8a88ac00000000',
'01000000045fbc74110c2d6fcf4d1161a59913fbcd2b6ab3c5a9eb4d0dc0859515cbc8654f000000006b48304502210080ffbde0d510c3fb9abcc5f7570448e9c0f7138d0b355d00bb97cada0679ac9502207ffd205373829c800ec08079a4280c3abe6f6f8c94ae7af0157a14ea5629d28701210316e84a2556f30a199541633f5dda6787710ccab26771b7084f4c9e1104f47667ffffffff5fbc74110c2d6fcf4d1161a59913fbcd2b6ab3c5a9eb4d0dc0859515cbc8654f010000006a473044022077788d7e118802fd7268aac7a1dde5a6724f01936e23edd46ac2750fd39265be0220776ac9e4c285580d06510a00b561cec6de1813293e7b04b6f870138af832bf9e012102ad7b2216f3a2b38d56db8a7ee5c540fd12c4bbb7013106eff78cc2ace65aa002ffffffff5fbc74110c2d6fcf4d1161a59913fbcd2b6ab3c5a9eb4d0dc0859515cbc8654f020000006b4830450221009e47b48dd1eee6d00a1817480605f446e11949b1e6f464f43f04bce2fc787ea9022022b3dcf80e7b2c995cf6defb3425b57d8a80918c7f543faaa0497d853820779101210316e84a2556f30a199541633f5dda6787710ccab26771b7084f4c9e1104f47667ffffffff5fbc74110c2d6fcf4d1161a59913fbcd2b6ab3c5a9eb4d0dc0859515cbc8654f030000006b48304502210089c20d6c0f6486c5979cf69a3c849f09e36416e5604499c05ae2dc22bea8553d022011241a206d550e55b4476ac5ba0fd744f0965d8f8bd69a740e428770689749a1012102ad7b2216f3a2b38d56db8a7ee5c540fd12c4bbb7013106eff78cc2ace65aa002ffffffff02803801000000000017a914a3a65daca3064280ae072b9d6773c027b30abace872c4c0000000000001976a9146ee5e3e66dc73587a3a2d77a1a6c8554fae21b8a88ac00000000',
);
var tx = bitcoin.Transaction.fromHex(txhex);
@ -346,22 +345,7 @@ it.skip('Legacy HD (BIP44) can create TX', async () => {
assert.strictEqual(tx.outs[0].value, 99800);
});
it('Legacy HD (BIP44) can fetch UTXO', async function() {
let hd = new HDLegacyP2PKHWallet();
hd.usedAddresses = ['1Ez69SnzzmePmZX3WpEzMKTrcBF2gpNQ55', '1BiTCHeYzJNMxBLFCMkwYXNdFEdPJP53ZV']; // hacking internals
await hd.fetchUtxo();
assert.ok(hd.utxo.length >= 12);
assert.ok(typeof hd.utxo[0].confirmations === 'number');
assert.ok(hd.utxo[0].txid);
assert.ok(hd.utxo[0].vout);
assert.ok(hd.utxo[0].amount);
assert.ok(
hd.utxo[0].address &&
(hd.utxo[0].address === '1Ez69SnzzmePmZX3WpEzMKTrcBF2gpNQ55' || hd.utxo[0].address === '1BiTCHeYzJNMxBLFCMkwYXNdFEdPJP53ZV'),
);
});
it.skip('HD breadwallet works', async function() {
it('HD breadwallet works', async function() {
if (!process.env.HD_MNEMONIC_BREAD) {
console.error('process.env.HD_MNEMONIC_BREAD not set, skipped');
return;
@ -379,17 +363,17 @@ it.skip('HD breadwallet works', async function() {
'xpub68nLLEi3KERQY7jyznC9PQSpSjmekrEmN8324YRCXayMXaavbdEJsK4gEcX2bNf9vGzT4xRks9utZ7ot1CTHLtdyCn9udvv1NWvtY7HXroh',
);
await hdBread.fetchBalance();
assert.strictEqual(hdBread.balance, 0);
assert.strictEqual(hdBread.getBalance(), 123456);
assert.ok(hdBread._lastTxFetch === 0);
await hdBread.fetchTransactions();
assert.ok(hdBread._lastTxFetch > 0);
assert.strictEqual(hdBread.getTransactions().length, 177);
assert.strictEqual(hdBread.getTransactions().length, 178);
for (let tx of hdBread.getTransactions()) {
assert.ok(tx.confirmations);
}
assert.strictEqual(hdBread.next_free_address_index, 10);
assert.strictEqual(hdBread.next_free_address_index, 11);
assert.strictEqual(hdBread.next_free_change_address_index, 118);
// checking that internal pointer and async address getter return the same address

154
tests/integration/LegacyWallet.test.js

@ -0,0 +1,154 @@
/* global describe, it, jasmine, afterAll, beforeAll */
import { LegacyWallet, SegwitP2SHWallet, SegwitBech32Wallet } from '../../class';
let assert = require('assert');
global.net = require('net'); // needed by Electrum client. For RN it is proviced in shim.js
let BlueElectrum = require('../../BlueElectrum'); // so it connects ASAP
jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000;
afterAll(async () => {
// after all tests we close socket so the test suite can actually terminate
BlueElectrum.forceDisconnect();
return new Promise(resolve => setTimeout(resolve, 10000)); // simple sleep to wait for all timeouts termination
});
beforeAll(async () => {
// awaiting for Electrum to be connected. For RN Electrum would naturally connect
// while app starts up, but for tests we need to wait for it
await BlueElectrum.waitTillConnected();
});
describe('LegacyWallet', function() {
it('can serialize and unserialize correctly', () => {
let a = new LegacyWallet();
a.setLabel('my1');
let key = JSON.stringify(a);
let b = LegacyWallet.fromJson(key);
assert(key === JSON.stringify(b));
assert.strictEqual(key, JSON.stringify(b));
});
it('can validate addresses', () => {
let w = new LegacyWallet();
assert.ok(w.isAddressValid('12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG'));
assert.ok(!w.isAddressValid('12eQ9m4sgAwTSQoNXkRABKhCXCsjm2j'));
assert.ok(w.isAddressValid('3BDsBDxDimYgNZzsqszNZobqQq3yeUoJf2'));
assert.ok(!w.isAddressValid('3BDsBDxDimYgNZzsqszNZobqQq3yeUo'));
assert.ok(!w.isAddressValid('12345'));
assert.ok(w.isAddressValid('bc1quuafy8htjjj263cvpj7md84magzmc8svmh8lrm'));
assert.ok(w.isAddressValid('BC1QH6TF004TY7Z7UN2V5NTU4MKF630545GVHS45U7'));
});
it('can fetch balance', async () => {
let w = new LegacyWallet();
w._address = '115fUy41sZkAG14CmdP1VbEKcNRZJWkUWG'; // hack internals
assert.ok(w.weOwnAddress('115fUy41sZkAG14CmdP1VbEKcNRZJWkUWG'));
assert.ok(!w.weOwnAddress('aaa'));
assert.ok(w.getBalance() === 0);
assert.ok(w.getUnconfirmedBalance() === 0);
assert.ok(w._lastBalanceFetch === 0);
await w.fetchBalance();
assert.ok(w.getBalance() === 18262000);
assert.ok(w.getUnconfirmedBalance() === 0);
assert.ok(w._lastBalanceFetch > 0);
});
it('can fetch TXs', async () => {
let w = new LegacyWallet();
w._address = '12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG';
await w.fetchTransactions();
assert.strictEqual(w.getTransactions().length, 2);
for (let tx of w.getTransactions()) {
assert.ok(tx.hash);
assert.ok(tx.value);
assert.ok(tx.received);
assert.ok(tx.confirmations > 1);
}
});
it('can fetch UTXO', async () => {
let w = new LegacyWallet();
w._address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX';
await w.fetchUtxo();
assert.ok(w.utxo.length > 0, 'unexpected empty UTXO');
assert.ok(w.getUtxo().length > 0, 'unexpected empty UTXO');
assert.ok(w.getUtxo()[0]['value']);
assert.ok(w.getUtxo()[0]['tx_output_n'] === 0 || w.getUtxo()[0]['tx_output_n'] === 1, JSON.stringify(w.getUtxo()[0]));
assert.ok(w.getUtxo()[0]['tx_hash']);
assert.ok(w.getUtxo()[0]['confirmations']);
});
});
describe('SegwitP2SHWallet', function() {
it('can generate segwit P2SH address from WIF', async () => {
let l = new SegwitP2SHWallet();
l.setSecret('Kxr9tQED9H44gCmp6HAdmemAzU3n84H3dGkuWTKvE23JgHMW8gct');
assert.ok(l.getAddress() === '34AgLJhwXrvmkZS1o5TrcdeevMt22Nar53', 'expected ' + l.getAddress());
assert.ok(l.getAddress() === (await l.getAddressAsync()));
assert.ok(l.weOwnAddress('34AgLJhwXrvmkZS1o5TrcdeevMt22Nar53'));
});
});
describe('SegwitBech32Wallet', function() {
it('can fetch balance', async () => {
let w = new SegwitBech32Wallet();
w._address = 'bc1qn887fmetaytw4vj68vsh529ft408q8j9x3dndc';
assert.ok(w.weOwnAddress('bc1qn887fmetaytw4vj68vsh529ft408q8j9x3dndc'));
await w.fetchBalance();
assert.strictEqual(w.getBalance(), 100000);
});
it('can fetch UTXO', async () => {
let w = new SegwitBech32Wallet();
w._address = 'bc1qn887fmetaytw4vj68vsh529ft408q8j9x3dndc';
await w.fetchUtxo();
assert.ok(w.getUtxo().length > 0, 'unexpected empty UTXO');
assert.ok(w.getUtxo()[0]['value']);
assert.ok(w.getUtxo()[0]['tx_output_n'] === 0);
assert.ok(w.getUtxo()[0]['tx_hash']);
assert.ok(w.getUtxo()[0]['confirmations']);
});
it('can fetch TXs', async () => {
let w = new LegacyWallet();
w._address = 'bc1quhnve8q4tk3unhmjts7ymxv8cd6w9xv8wy29uv';
await w.fetchTransactions();
assert.strictEqual(w.getTransactions().length, 2);
for (let tx of w.getTransactions()) {
assert.ok(tx.hash);
assert.ok(tx.value);
assert.ok(tx.received);
assert.ok(tx.confirmations > 1);
}
assert.strictEqual(w.getTransactions()[0].value, -892111);
assert.strictEqual(w.getTransactions()[1].value, 892111);
});
it('can fetch TXs', async () => {
let w = new LegacyWallet();
w._address = 'bc1qn887fmetaytw4vj68vsh529ft408q8j9x3dndc';
assert.ok(w.weOwnAddress('bc1qn887fmetaytw4vj68vsh529ft408q8j9x3dndc'));
await w.fetchTransactions();
assert.strictEqual(w.getTransactions().length, 1);
for (let tx of w.getTransactions()) {
assert.ok(tx.hash);
assert.strictEqual(tx.value, 100000);
assert.ok(tx.received);
assert.ok(tx.confirmations > 1);
}
let tx0 = w.getTransactions()[0];
assert.ok(tx0['inputs']);
assert.ok(tx0['inputs'].length === 1);
assert.ok(tx0['outputs']);
assert.ok(tx0['outputs'].length === 3);
});
});

76
tests/integration/WatchOnlyWallet.test.js

@ -16,6 +16,8 @@ beforeAll(async () => {
await BlueElectrum.waitTillConnected();
});
jasmine.DEFAULT_TIMEOUT_INTERVAL = 500 * 1000;
describe('Watch only wallet', () => {
it('can fetch balance', async () => {
let w = new WatchOnlyWallet();
@ -25,12 +27,11 @@ describe('Watch only wallet', () => {
});
it('can fetch tx', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 150 * 1000;
let w = new WatchOnlyWallet();
w.setSecret('167zK5iZrs1U6piDqubD3FjRqUTM2CZnb8');
await w.fetchTransactions();
assert.strictEqual(w.getTransactions().length, 233);
assert.ok(w.getTransactions().length >= 215);
// should be 233 but electrum server cant return huge transactions >.<
w = new WatchOnlyWallet();
w.setSecret('1BiJW1jyUaxcJp2JWwbPLPzB1toPNWTFJV');
@ -42,8 +43,33 @@ describe('Watch only wallet', () => {
assert.strictEqual(w.getTransactions().length, 2);
});
it('can fetch TXs with values', async () => {
let w = new WatchOnlyWallet();
for (let sec of [
'bc1quhnve8q4tk3unhmjts7ymxv8cd6w9xv8wy29uv',
'BC1QUHNVE8Q4TK3UNHMJTS7YMXV8CD6W9XV8WY29UV',
'bitcoin:bc1quhnve8q4tk3unhmjts7ymxv8cd6w9xv8wy29uv',
'BITCOIN:BC1QUHNVE8Q4TK3UNHMJTS7YMXV8CD6W9XV8WY29UV',
]) {
w.setSecret(sec);
assert.strictEqual(w.getAddress(), 'bc1quhnve8q4tk3unhmjts7ymxv8cd6w9xv8wy29uv');
assert.strictEqual(await w.getAddressAsync(), 'bc1quhnve8q4tk3unhmjts7ymxv8cd6w9xv8wy29uv');
assert.ok(w.weOwnAddress('bc1quhnve8q4tk3unhmjts7ymxv8cd6w9xv8wy29uv'));
await w.fetchTransactions();
for (let tx of w.getTransactions()) {
assert.ok(tx.hash);
assert.ok(tx.value);
assert.ok(tx.received);
assert.ok(tx.confirmations > 1);
}
assert.strictEqual(w.getTransactions()[0].value, -892111);
assert.strictEqual(w.getTransactions()[1].value, 892111);
}
});
it('can fetch complex TXs', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 120 * 1000;
let w = new WatchOnlyWallet();
w.setSecret('3NLnALo49CFEF4tCRhCvz45ySSfz3UktZC');
await w.fetchTransactions();
@ -54,28 +80,34 @@ describe('Watch only wallet', () => {
it('can validate address', async () => {
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);
for (let secret of [
'bc1quhnve8q4tk3unhmjts7ymxv8cd6w9xv8wy29uv',
'12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG',
'3BDsBDxDimYgNZzsqszNZobqQq3yeUoJf2',
'BC1QUHNVE8Q4TK3UNHMJTS7YMXV8CD6W9XV8WY29UV',
]) {
w.setSecret(secret);
assert.ok(w.valid());
assert.strictEqual(w.isHd(), false);
}
w.setSecret('not valid');
assert.ok(!w.valid());
w.setSecret('xpub6CQdfC3v9gU86eaSn7AhUFcBVxiGhdtYxdC5Cw2vLmFkfth2KXCMmYcPpvZviA89X6DXDs4PJDk5QVL2G2xaVjv7SM4roWHr1gR4xB3Z7Ps');
assert.ok(w.valid());
w.setSecret('ypub6XRzrn3HB1tjhhvrHbk1vnXCecZEdXohGzCk3GXwwbDoJ3VBzZ34jNGWbC6WrS7idXrYjjXEzcPDX5VqnHEnuNf5VAXgLfSaytMkJ2rwVqy');
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');
for (let secret of [
'xpub6CQdfC3v9gU86eaSn7AhUFcBVxiGhdtYxdC5Cw2vLmFkfth2KXCMmYcPpvZviA89X6DXDs4PJDk5QVL2G2xaVjv7SM4roWHr1gR4xB3Z7Ps',
'ypub6XRzrn3HB1tjhhvrHbk1vnXCecZEdXohGzCk3GXwwbDoJ3VBzZ34jNGWbC6WrS7idXrYjjXEzcPDX5VqnHEnuNf5VAXgLfSaytMkJ2rwVqy',
'zpub6r7jhKKm7BAVx3b3nSnuadY1WnshZYkhK8gKFoRLwK9rF3Mzv28BrGcCGA3ugGtawi1WLb2vyjQAX9ZTDGU5gNk2bLdTc3iEXr6tzR1ipNP',
]) {
w.setSecret(secret);
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 () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000;
let w = new WatchOnlyWallet();
w.setSecret('zpub6r7jhKKm7BAVx3b3nSnuadY1WnshZYkhK8gKFoRLwK9rF3Mzv28BrGcCGA3ugGtawi1WLb2vyjQAX9ZTDGU5gNk2bLdTc3iEXr6tzR1ipNP');
await w.fetchBalance();
@ -117,7 +149,6 @@ 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();
@ -175,7 +206,6 @@ describe('Watch only wallet', () => {
});
it('can fetch balance & transactions from ypub HD', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000;
let w = new WatchOnlyWallet();
w.setSecret('ypub6Y9u3QCRC1HkZv3stNxcQVwmw7vC7KX5Ldz38En5P88RQbesP2oy16hNyQocVCfYRQPxdHcd3pmu9AFhLv7NdChWmw5iNLryZ2U6EEHdnfo');
await w.fetchBalance();
@ -186,7 +216,6 @@ describe('Watch only wallet', () => {
});
it('can fetch balance & transactions from xpub HD', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000;
let w = new WatchOnlyWallet();
w.setSecret('xpub6CQdfC3v9gU86eaSn7AhUFcBVxiGhdtYxdC5Cw2vLmFkfth2KXCMmYcPpvZviA89X6DXDs4PJDk5QVL2G2xaVjv7SM4roWHr1gR4xB3Z7Ps');
await w.fetchBalance();
@ -197,7 +226,6 @@ describe('Watch only wallet', () => {
});
it('can fetch large HD', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 500 * 1000;
let w = new WatchOnlyWallet();
w.setSecret('ypub6WnnYxkQCGeowv4BXq9Y9PHaXgHMJg9TkFaDJkunhcTAfbDw8z3LvV9kFNHGjeVaEoGdsSJgaMWpUBvYvpYGMJd43gTK5opecVVkvLwKttx');
await w.fetchBalance();

Loading…
Cancel
Save