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 version: 2
jobs: jobs:
build: build:
docker: docker:
# specify the version you desire here - image: circleci/node:10.16.3
- 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
working_directory: ~/repo working_directory: ~/repo
steps: steps:
- checkout - checkout
- restore_cache:
key: node_modules-{{ checksum "package-lock.json" }}
- run: npm i - run: npm i
- save_cache:
key: node_modules-{{ checksum "package-lock.json" }}
paths:
- node_modules
# run tests! # run tests!
- run: npm run test - run: npm run test

2
BlueComponents.js

@ -2089,7 +2089,7 @@ export class BlueAddressInput extends Component {
<TouchableOpacity <TouchableOpacity
disabled={this.props.isLoading} disabled={this.props.isLoading}
onPress={() => { 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(); Keyboard.dismiss();
}} }}
style={{ style={{

4
BlueElectrum.js

@ -295,7 +295,7 @@ module.exports.multiGetHistoryByAddress = async function(addresses, batchsize) {
}; };
module.exports.multiGetTransactionByTxid = async function(txids, batchsize, verbose) { 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 // 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 // throw 'response too large (over 1,000,000 bytes', test suite will pass
verbose = verbose !== false; verbose = verbose !== false;
@ -341,7 +341,7 @@ module.exports.waitTillConnected = async function() {
clearInterval(waitTillConnectedInterval); clearInterval(waitTillConnectedInterval);
reject(new Error('Waiting for Electrum connection timeout')); 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 details from './screen/transactions/details';
import TransactionStatus from './screen/transactions/transactionStatus'; 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 cpfp from './screen/transactions/CPFP';
import rbfBumpFee from './screen/transactions/RBFBumpFee'; import rbfBumpFee from './screen/transactions/RBFBumpFee';
import rbfCancel from './screen/transactions/RBFCancel'; import rbfCancel from './screen/transactions/RBFCancel';
@ -37,7 +35,7 @@ import rbfCancel from './screen/transactions/RBFCancel';
import receiveDetails from './screen/receive/details'; import receiveDetails from './screen/receive/details';
import sendDetails from './screen/send/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 sendCreate from './screen/send/create';
import Confirm from './screen/send/confirm'; import Confirm from './screen/send/confirm';
import PsbtWithHardwareWallet from './screen/send/psbtWithHardwareWallet'; import PsbtWithHardwareWallet from './screen/send/psbtWithHardwareWallet';
@ -78,12 +76,6 @@ const WalletsStackNavigator = createStackNavigator(
WalletDetails: { WalletDetails: {
screen: WalletDetails, screen: WalletDetails,
}, },
RBF: {
screen: rbf,
},
CreateRBF: {
screen: createrbf,
},
CPFP: { CPFP: {
screen: cpfp, screen: cpfp,
}, },
@ -259,7 +251,7 @@ const HandleOffchainAndOnChainStackNavigator = createStackNavigator(
header: null, header: null,
}, },
}, },
ScanQrAddress: { ScanQRCode: {
screen: ScanQRCode, screen: ScanQRCode,
}, },
SendDetails: { SendDetails: {
@ -330,7 +322,7 @@ const MainBottomTabs = createStackNavigator(
header: null, header: null,
}, },
}, },
ScanQrAddress: { ScanQRCode: {
screen: ScanQRCode, screen: ScanQRCode,
}, },
LappBrowser: { LappBrowser: {

2
android/app/build.gradle

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

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

@ -635,22 +635,39 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
} }
async fetchUtxo() { async fetchUtxo() {
// considering only confirmed balance // fetching utxo of addresses that only have some balance
// also, fetching utxo of addresses that only have some balance
let addressess = []; let addressess = [];
// considering confirmed balance:
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
if (this._balances_by_external_index[c] && this._balances_by_external_index[c].c && this._balances_by_external_index[c].c > 0) { 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)); addressess.push(this._getExternalAddressByIndex(c));
} }
} }
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; 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) { 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)); 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 = []; this._utxo = [];
for (let arr of Object.values(await BlueElectrum.multiGetUtxoByAddress(addressess))) { for (let arr of Object.values(await BlueElectrum.multiGetUtxoByAddress(addressess))) {
this._utxo = this._utxo.concat(arr); this._utxo = this._utxo.concat(arr);
@ -665,8 +682,25 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
u.wif = this._getWifForAddress(u.address); u.wif = this._getWifForAddress(u.address);
u.confirmations = u.height ? 1 : 0; 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() { getUtxo() {
return this._utxo; return this._utxo;
} }

3
class/abstract-hd-wallet.js

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

9
class/abstract-wallet.js

@ -105,7 +105,7 @@ export class AbstractWallet {
} }
weOwnAddress(address) { weOwnAddress(address) {
return this._address === address; throw Error('not implemented');
} }
/** /**
@ -128,7 +128,12 @@ export class AbstractWallet {
} }
setSecret(newSecret) { 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 { try {
const parsedSecret = JSON.parse(this.secret); const parsedSecret = JSON.parse(this.secret);

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

@ -1,5 +1,4 @@
import { AbstractHDWallet } from './abstract-hd-wallet'; import { AbstractHDElectrumWallet } from './abstract-hd-electrum-wallet';
import Frisbee from 'frisbee';
import bip39 from 'bip39'; import bip39 from 'bip39';
const bip32 = require('bip32'); const bip32 = require('bip32');
const bitcoinjs = require('bitcoinjs-lib'); const bitcoinjs = require('bitcoinjs-lib');
@ -8,7 +7,7 @@ const bitcoinjs = require('bitcoinjs-lib');
* HD Wallet (BIP39). * HD Wallet (BIP39).
* In particular, Breadwallet-compatible (Legacy addresses) * In particular, Breadwallet-compatible (Legacy addresses)
*/ */
export class HDLegacyBreadwalletWallet extends AbstractHDWallet { export class HDLegacyBreadwalletWallet extends AbstractHDElectrumWallet {
static type = 'HDLegacyBreadwallet'; static type = 'HDLegacyBreadwallet';
static typeReadable = 'HD Legacy Breadwallet (P2PKH)'; static typeReadable = 'HD Legacy Breadwallet (P2PKH)';
@ -35,15 +34,10 @@ export class HDLegacyBreadwalletWallet extends AbstractHDWallet {
_getExternalAddressByIndex(index) { _getExternalAddressByIndex(index) {
index = index * 1; // cast to int index = index * 1; // cast to int
if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit 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({ const address = bitcoinjs.payments.p2pkh({
pubkey: child.publicKey, pubkey: node.derive(0).derive(index).publicKey,
}).address; }).address;
return (this.external_addresses_cache[index] = address); return (this.external_addresses_cache[index] = address);
@ -52,15 +46,10 @@ export class HDLegacyBreadwalletWallet extends AbstractHDWallet {
_getInternalAddressByIndex(index) { _getInternalAddressByIndex(index) {
index = index * 1; // cast to int index = index * 1; // cast to int
if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit 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({ const address = bitcoinjs.payments.p2pkh({
pubkey: child.publicKey, pubkey: node.derive(1).derive(index).publicKey,
}).address; }).address;
return (this.internal_addresses_cache[index] = address); return (this.internal_addresses_cache[index] = address);
@ -90,26 +79,4 @@ export class HDLegacyBreadwalletWallet extends AbstractHDWallet {
return child.keyPair.toWIF(); 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 bip39 from 'bip39';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import signer from '../models/signer'; import signer from '../models/signer';
import { AbstractHDElectrumWallet } from './abstract-hd-electrum-wallet';
const bitcoin = require('bitcoinjs-lib'); const bitcoin = require('bitcoinjs-lib');
const HDNode = require('bip32'); const HDNode = require('bip32');
@ -10,7 +10,7 @@ const HDNode = require('bip32');
* In particular, BIP44 (P2PKH legacy addressess) * In particular, BIP44 (P2PKH legacy addressess)
* @see https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki * @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 type = 'HDlegacyP2PKH';
static typeReadable = 'HD Legacy (BIP44 P2PKH)'; static typeReadable = 'HD Legacy (BIP44 P2PKH)';

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

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

399
class/legacy-wallet.js

@ -1,7 +1,5 @@
import { AbstractWallet } from './abstract-wallet'; import { AbstractWallet } from './abstract-wallet';
import { SegwitBech32Wallet } from './'; import { HDSegwitBech32Wallet } from './';
import { useBlockcypherTokens } from './constants';
import Frisbee from 'frisbee';
import { NativeModules } from 'react-native'; import { NativeModules } from 'react-native';
const bitcoin = require('bitcoinjs-lib'); const bitcoin = require('bitcoinjs-lib');
const { RNRandomBytes } = NativeModules; const { RNRandomBytes } = NativeModules;
@ -37,7 +35,7 @@ export class LegacyWallet extends AbstractWallet {
* @return {boolean} * @return {boolean}
*/ */
timeToRefreshTransaction() { timeToRefreshTransaction() {
for (let tx of this.transactions) { for (let tx of this.getTransactions()) {
if (tx.confirmations < 7) { if (tx.confirmations < 7) {
return true; return true;
} }
@ -104,24 +102,13 @@ export class LegacyWallet extends AbstractWallet {
*/ */
async fetchBalance() { async fetchBalance() {
try { try {
const api = new Frisbee({ let balance = await BlueElectrum.getBalanceByAddress(this.getAddress());
baseURI: 'https://api.blockcypher.com/v1/btc/main/addrs/', this.balance = Number(balance.confirmed);
}); this.unconfirmed_balance = new BigNumber(balance.unconfirmed);
this.unconfirmed_balance = this.unconfirmed_balance.dividedBy(100000000).toString() * 1; // wtf
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;
this._lastBalanceFetch = +new Date(); this._lastBalanceFetch = +new Date();
} catch (err) { } catch (Error) {
console.warn(err); console.warn(Error);
} }
} }
@ -131,230 +118,116 @@ export class LegacyWallet extends AbstractWallet {
* @return {Promise.<void>} * @return {Promise.<void>}
*/ */
async fetchUtxo() { async fetchUtxo() {
const api = new Frisbee({
baseURI: 'https://api.blockcypher.com/v1/btc/main/addrs/',
});
let response;
try { try {
let maxHeight = 0; let utxos = await BlueElectrum.multiGetUtxoByAddress([this.getAddress()]);
this.utxo = []; for (let arr of Object.values(utxos)) {
let json; this.utxo = this.utxo.concat(arr);
}
do { } catch (Error) {
response = await api.get( console.warn(Error);
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);
json.unconfirmed_txrefs = json.unconfirmed_txrefs || []; // backward compatibility
this.utxo = this.utxo.concat(json.unconfirmed_txrefs); for (let u of this.utxo) {
} catch (err) { u.tx_output_n = u.vout;
console.warn(err); u.tx_hash = u.txId;
u.confirmations = u.height ? 1 : 0;
} }
} }
getUtxo() {
return this.utxo;
}
/** /**
* Fetches transactions via API. Returns VOID. * Fetches transactions via Electrum. Returns VOID.
* Use getter to get the actual list. * Use getter to get the actual list. *
* @see AbstractHDElectrumWallet.fetchTransactions()
* *
* @return {Promise.<void>} * @return {Promise.<void>}
*/ */
async fetchTransactions() { async fetchTransactions() {
try { // Below is a simplified copypaste from HD electrum wallet
const api = new Frisbee({ this._txs_by_external_index = [];
baseURI: 'https://api.blockcypher.com/', let addresses2fetch = [this.getAddress()];
});
// first: batch fetch for all addresses histories
let after = 0; let histories = await BlueElectrum.multiGetHistoryByAddress(addresses2fetch);
let before = 100500100; let txs = {};
for (let history of Object.values(histories)) {
for (let oldTx of this.getTransactions()) { for (let tx of history) {
if (oldTx.block_height && oldTx.confirmations < 7) { txs[tx.tx_hash] = tx;
after = Math.max(after, oldTx.block_height);
}
} }
}
while (1) { // next, batch fetching each txid we got
let response = await api.get( let txdatas = await BlueElectrum.multiGetTransactionByTxid(Object.keys(txs));
'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);
let txsUnconf = []; // now, tricky part. we collect all transactions from inputs (vin), and batch fetch them too.
let txs = []; // then we combine all this data (we need inputs to see source addresses and amounts)
let hashPresent = {}; let vinTxids = [];
// now, rearranging TXs. unconfirmed go first: for (let txdata of Object.values(txdatas)) {
for (let tx of this.transactions.reverse()) { for (let vin of txdata.vin) {
if (hashPresent[tx.hash]) continue; vinTxids.push(vin.txid);
hashPresent[tx.hash] = 1; }
if (tx.block_height && tx.block_height === -1) { }
// unconfirmed let vintxdatas = await BlueElectrum.multiGetTransactionByTxid(vinTxids);
console.log(tx);
if (+new Date(tx.received) < +new Date() - 3600 * 24 * 1000) { // fetched all transactions from our inputs. now we need to combine it.
// nop, too old unconfirmed tx - skipping it // iterating all _our_ transactions:
} else { for (let txid of Object.keys(txdatas)) {
txsUnconf.push(tx); // iterating all inputs our our single transaction:
} for (let inpNum = 0; inpNum < txdatas[txid].vin.length; inpNum++) {
} else { let inpTxid = txdatas[txid].vin[inpNum].txid;
txs.push(tx); 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]) {
this.transactions = txsUnconf.reverse().concat(txs.reverse()); // extracting amount & addresses from previous output and adding it to _our_ input:
// all reverses needed so freshly fetched TXs replace same old TXs txdatas[txid].vin[inpNum].addresses = vintxdatas[inpTxid].vout[inpVout].scriptPubKey.addresses;
txdatas[txid].vin[inpNum].value = vintxdatas[inpTxid].vout[inpVout].value;
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;
} }
} }
} catch (err) {
console.warn(err);
} }
}
async _fetchAdditionalOutputs(nextOutputs) { // now, we need to put transactions in all relevant `cells` of internal hashmaps: this.transactions_by_internal_index && this.transactions_by_external_index
let outputs = [];
let baseURI = nextOutputs.split('/');
baseURI = baseURI[0] + '/' + baseURI[1] + '/' + baseURI[2] + '/';
const api = new Frisbee({
baseURI: baseURI,
});
do { for (let tx of Object.values(txdatas)) {
await (() => new Promise(resolve => setTimeout(resolve, 1000)))(); for (let vin of tx.vin) {
nextOutputs = nextOutputs.replace(baseURI, ''); 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()) || '')); this._txs_by_external_index.push(clonedTx);
let json = response.body; }
if (typeof json === 'undefined') {
throw new Error('Could not fetch transactions from API:' + response.err);
} }
for (let vout of tx.vout) {
if (json.outputs && json.outputs.length) { if (vout.scriptPubKey.addresses.indexOf(this.getAddress()) !== -1) {
outputs = outputs.concat(json.outputs); // this TX is related to our address
nextOutputs = json.next_outputs; let clonedTx = Object.assign({}, tx);
} else { clonedTx.inputs = tx.vin.slice(0);
break; 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) { getTransactions() {
let inputs = []; // a hacky code reuse from electrum HD wallet:
let baseURI = nextInputs.split('/'); this._txs_by_external_index = this._txs_by_external_index || [];
baseURI = baseURI[0] + '/' + baseURI[1] + '/' + baseURI[2] + '/'; this._txs_by_internal_index = [];
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);
}
if (json.inputs && json.inputs.length) { let hd = new HDSegwitBech32Wallet();
inputs = inputs.concat(json.inputs); return hd.getTransactions.apply(this);
nextInputs = json.next_inputs;
} else {
break;
}
} while (1);
return inputs;
} }
async broadcastTx(txhex) { 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. * format expected by signer module, creates tx and returns signed string txhex.
* *
* @param utxos Unspent outputs, expects blockcypher format * @param utxos Unspent outputs, expects blockcypher format
@ -462,22 +277,12 @@ export class LegacyWallet extends AbstractWallet {
return new Date(max).toString(); return new Date(max).toString();
} }
getRandomBlockcypherToken() { /**
return (array => { * Validates any address, including legacy, p2sh and bech32
for (let i = array.length - 1; i > 0; i--) { *
let j = Math.floor(Math.random() * (i + 1)); * @param address
[array[i], array[j]] = [array[j], array[i]]; * @returns {boolean}
} */
return array[0];
})([
'0326b7107b4149559d18ce80612ef812',
'a133eb7ccacd4accb80cb1225de4b155',
'7c2b1628d27b4bd3bf8eaee7149c577f',
'f1e5a02b9ec84ec4bc8db2349022e5f5',
'e5926dbeb57145979153adc41305b183',
]);
}
isAddressValid(address) { isAddressValid(address) {
try { try {
bitcoin.address.toOutputScript(address); bitcoin.address.toOutputScript(address);
@ -486,4 +291,8 @@ export class LegacyWallet extends AbstractWallet {
return false; 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; let address;
try { try {
let keyPair = bitcoin.ECPair.fromWIF(this.secret); 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({ address = bitcoin.payments.p2wpkh({
pubkey: keyPair.publicKey, pubkey: keyPair.publicKey,
}).address; }).address;

4
class/segwit-p2sh-wallet.js

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

10
class/walletImport.js

@ -125,7 +125,7 @@ export default class WalletImport {
if (hd4.validateMnemonic()) { if (hd4.validateMnemonic()) {
await hd4.fetchBalance(); await hd4.fetchBalance();
if (hd4.getBalance() > 0) { 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); return WalletImport._saveWallet(hd4);
} }
} }
@ -168,7 +168,7 @@ export default class WalletImport {
if (hd1.validateMnemonic()) { if (hd1.validateMnemonic()) {
await hd1.fetchBalance(); await hd1.fetchBalance();
if (hd1.getBalance() > 0) { 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); return WalletImport._saveWallet(hd1);
} }
} }
@ -178,7 +178,7 @@ export default class WalletImport {
if (hd2.validateMnemonic()) { if (hd2.validateMnemonic()) {
await hd2.fetchBalance(); await hd2.fetchBalance();
if (hd2.getBalance() > 0) { 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); return WalletImport._saveWallet(hd2);
} }
} }
@ -188,7 +188,7 @@ export default class WalletImport {
if (hd3.validateMnemonic()) { if (hd3.validateMnemonic()) {
await hd3.fetchBalance(); await hd3.fetchBalance();
if (hd3.getBalance() > 0) { 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); return WalletImport._saveWallet(hd3);
} }
} }
@ -230,7 +230,7 @@ export default class WalletImport {
let watchOnly = new WatchOnlyWallet(); let watchOnly = new WatchOnlyWallet();
watchOnly.setSecret(importText); watchOnly.setSecret(importText);
if (watchOnly.valid()) { 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(); await watchOnly.fetchBalance();
return WalletImport._saveWallet(watchOnly, additionalProperties); return WalletImport._saveWallet(watchOnly, additionalProperties);
} }

2
ios/BlueWallet/Info.plist

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

2
ios/BlueWalletWatch Extension/Info.plist

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

2
ios/BlueWalletWatch/Info.plist

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

60
ios/Podfile.lock

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

2
ios/TodayExtension/Info.plist

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>5.0.1</string> <string>5.1.0</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1</string> <string>1</string>
<key>NSExtension</key> <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 v5.0.0
====== ======
@ -40,19 +55,4 @@ v4.9.2
* FIX: Don't show wallet export warning if wallet was imported * FIX: Don't show wallet export warning if wallet was imported
* REF: Reworked Import wallet flow * REF: Reworked Import wallet flow
* REF: BIP49 to use electrum * REF: BIP49 to use electrum
* REF: Custom receive * 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

42
loc/de_DE.js

@ -25,8 +25,8 @@ module.exports = {
empty_txs1: 'Deine Transaktionen erscheinen hier', empty_txs1: 'Deine Transaktionen erscheinen hier',
empty_txs2: 'Noch keine Transaktionen', empty_txs2: 'Noch keine Transaktionen',
empty_txs1_lightning: empty_txs1_lightning:
'Lightning wallet should be used for your daily transactions. Fees are unfairly cheap and speed is blazing fast.', 'Verwende das Lightning Wallet für Deine täglichen Bezahlungen. Lightning Transaktionen sind konkurrenzlos günstig und verblüffend schnell.',
empty_txs2_lightning: '\nTo start using it tap on "manage funds" and topup your balance.', 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', tap_here_to_buy: 'Klicke hier, um Bitcoin zu kaufen',
}, },
reorder: { reorder: {
@ -52,7 +52,7 @@ module.exports = {
details: { details: {
title: 'Wallet', title: 'Wallet',
address: 'Adresse', address: 'Adresse',
master_fingerprint: 'Master fingerprint', master_fingerprint: 'Fingerabdruckerkennung',
type: 'Typ', type: 'Typ',
label: 'Bezeichnung', label: 'Bezeichnung',
destination: 'Zieladresse', destination: 'Zieladresse',
@ -105,7 +105,7 @@ module.exports = {
tabBarLabel: 'Transaktionen', tabBarLabel: 'Transaktionen',
title: 'Transaktionen', title: 'Transaktionen',
description: 'Eine Liste eingehender oder ausgehender Transaktionen deiner Wallets', description: 'Eine Liste eingehender oder ausgehender Transaktionen deiner Wallets',
conf: 'conf', conf: 'Konf',
}, },
details: { details: {
title: 'Transaktionen', title: 'Transaktionen',
@ -130,7 +130,7 @@ module.exports = {
fee_placeholder: 'plus Gebühr (in BTC)', fee_placeholder: 'plus Gebühr (in BTC)',
note_placeholder: 'Notiz', note_placeholder: 'Notiz',
cancel: 'Abbrechen', cancel: 'Abbrechen',
scan: 'Scan', scan: 'Scannen',
send: 'Senden', send: 'Senden',
create: 'Erstellen', create: 'Erstellen',
remaining_balance: 'Verfügbarer Betrag', remaining_balance: 'Verfügbarer Betrag',
@ -165,10 +165,10 @@ module.exports = {
share: 'Teilen', share: 'Teilen',
copiedToClipboard: 'In die Zwischenablage kopiert.', copiedToClipboard: 'In die Zwischenablage kopiert.',
label: 'Beschreibung', label: 'Beschreibung',
create: 'Create', create: 'Erstelle',
setAmount: 'Zu erhaltender Betrag', setAmount: 'Zu erhaltender Betrag',
}, },
scan_lnurl: 'Scan to receive', scan_lnurl: 'Scannen, zum Erhalten',
}, },
buyBitcoin: { buyBitcoin: {
header: 'Kaufe Bitcoin', header: 'Kaufe Bitcoin',
@ -190,14 +190,14 @@ module.exports = {
'Bitte installier Lndhub, um mit deiner eigenen LND Node zu verbinden' + '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- ' + ' und setz seine URL hier in den Einstellungen. Lass das Feld leer, um Standard- ' +
'LndHub\n (lndhub.io) zu verwenden', 'LndHub\n (lndhub.io) zu verwenden',
electrum_settings: 'Electrum Settings', electrum_settings: 'Electrum Einstellungen',
electrum_settings_explain: 'Set to blank to use default', electrum_settings_explain: 'Leer lassen, um den Standard zu verwenden.',
save: 'Speichern', save: 'Speichern',
about: 'Über', about: 'Über',
language: 'Sprache', language: 'Sprache',
currency: 'Währung', currency: 'Währung',
advanced_options: 'Advanced Options', advanced_options: 'Erweiterte Optionen',
enable_advanced_mode: 'Enable advanced mode', enable_advanced_mode: 'Erweiterter Modus verwenden',
}, },
plausibledeniability: { plausibledeniability: {
title: 'Glaubhafte Täuschung', title: 'Glaubhafte Täuschung',
@ -226,24 +226,24 @@ module.exports = {
refill_lnd_balance: 'Lade deine Lightning Wallet auf', refill_lnd_balance: 'Lade deine Lightning Wallet auf',
refill: 'Aufladen', refill: 'Aufladen',
withdraw: 'Abheben', withdraw: 'Abheben',
expired: 'Expired', expired: 'Abgelaufen',
placeholder: 'Invoice', placeholder: 'Rechnung',
sameWalletAsInvoiceError: sameWalletAsInvoiceError:
'Du kannst nicht die Rechnung mit der Wallet begleichen, die du für die Erstellung dieser Rechnung verwendet hast.', 'Du kannst nicht die Rechnung mit der Wallet begleichen, die du für die Erstellung dieser Rechnung verwendet hast.',
}, },
pleasebackup: { pleasebackup: {
title: 'Your wallet is created...', title: 'Ihr Wallet wird erstellt...',
text: 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.", '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: 'OK, I wrote this down!', ok: 'Ja, mein Geld ist sicher!',
}, },
lndViewInvoice: { lndViewInvoice: {
wasnt_paid_and_expired: 'This invoice was not paid for and has expired', wasnt_paid_and_expired: 'Diese Rechnung ist unbezahlt und abgelaufen.',
has_been_paid: 'This invoice has been paid for', has_been_paid: 'Diese Rechnung wurde bezahlt.',
please_pay: 'Please pay', please_pay: 'Bitte zahle',
sats: 'sats', sats: 'sats',
for: 'For:', for: 'r:',
additional_info: 'Additional Information', 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", "name": "BlueWallet",
"version": "5.0.1", "version": "5.1.0",
"devDependencies": { "devDependencies": {
"@babel/core": "^7.5.0", "@babel/core": "^7.5.0",
"@babel/runtime": "^7.5.1", "@babel/runtime": "^7.5.1",
@ -52,31 +52,31 @@
] ]
}, },
"dependencies": { "dependencies": {
"@babel/preset-env": "7.5.0", "@babel/preset-env": "7.8.4",
"@react-native-community/async-storage": "1.6.2", "@react-native-community/async-storage": "1.7.1",
"@react-native-community/blur": "3.4.1", "@react-native-community/blur": "3.4.1",
"@react-native-community/slider": "2.0.8", "@react-native-community/slider": "2.0.8",
"@remobile/react-native-qrcode-local-image": "git+https://github.com/BlueWallet/react-native-qrcode-local-image.git", "@remobile/react-native-qrcode-local-image": "git+https://github.com/BlueWallet/react-native-qrcode-local-image.git",
"@sentry/react-native": "1.2.1", "@sentry/react-native": "1.3.1",
"amplitude-js": "5.6.0", "amplitude-js": "5.9.0",
"bech32": "1.1.3", "bech32": "1.1.3",
"bignumber.js": "9.0.0", "bignumber.js": "9.0.0",
"bip21": "2.0.2", "bip21": "2.0.2",
"bip32": "2.0.3", "bip32": "2.0.5",
"bip39": "2.5.0", "bip39": "2.5.0",
"bitcoinjs-lib": "5.1.6", "bitcoinjs-lib": "5.1.6",
"bolt11": "1.2.7", "bolt11": "1.2.7",
"buffer": "5.2.1", "buffer": "5.4.3",
"buffer-reverse": "1.0.1", "buffer-reverse": "1.0.1",
"coinselect": "3.1.11", "coinselect": "3.1.11",
"crypto-js": "3.1.9-1", "crypto-js": "3.1.9-1",
"dayjs": "1.8.14", "dayjs": "1.8.20",
"ecurve": "1.0.6", "ecurve": "1.0.6",
"electrum-client": "git+https://github.com/BlueWallet/rn-electrum-client.git", "electrum-client": "git+https://github.com/BlueWallet/rn-electrum-client.git",
"eslint-config-prettier": "6.10.0", "eslint-config-prettier": "6.10.0",
"eslint-config-standard": "12.0.0", "eslint-config-standard": "12.0.0",
"eslint-config-standard-react": "7.0.2", "eslint-config-standard-react": "7.0.2",
"eslint-plugin-prettier": "3.1.0", "eslint-plugin-prettier": "3.1.2",
"eslint-plugin-standard": "4.0.0", "eslint-plugin-standard": "4.0.0",
"events": "1.1.1", "events": "1.1.1",
"frisbee": "2.0.9", "frisbee": "2.0.9",
@ -85,50 +85,50 @@
"mocha": "5.2.0", "mocha": "5.2.0",
"node-libs-react-native": "1.0.3", "node-libs-react-native": "1.0.3",
"path-browserify": "1.0.0", "path-browserify": "1.0.0",
"prettier": "1.18.2", "prettier": "1.19.1",
"process": "0.11.10", "process": "0.11.10",
"prop-types": "15.7.2", "prop-types": "15.7.2",
"react": "16.8.6", "react": "16.8.6",
"react-localization": "1.0.13", "react-localization": "1.0.15",
"react-native": "0.60.5", "react-native": "0.60.5",
"react-native-biometrics": "git+https://github.com/BlueWallet/react-native-biometrics.git#2.0.0", "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-default-preference": "1.4.1",
"react-native-device-info": "4.0.1", "react-native-device-info": "4.0.1",
"react-native-document-picker": "git+https://github.com/BlueWallet/react-native-document-picker.git#9ce83792db340d01b1361d24b19613658abef4aa", "react-native-document-picker": "git+https://github.com/BlueWallet/react-native-document-picker.git#9ce83792db340d01b1361d24b19613658abef4aa",
"react-native-elements": "0.19.0", "react-native-elements": "0.19.0",
"react-native-flexi-radio-button": "0.2.2", "react-native-flexi-radio-button": "0.2.2",
"react-native-fs": "2.13.3", "react-native-fs": "2.16.4",
"react-native-gesture-handler": "1.3.0", "react-native-gesture-handler": "1.5.6",
"react-native-handoff": "git+https://github.com/marcosrdz/react-native-handoff.git", "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-image-picker": "1.1.0",
"react-native-level-fs": "3.0.1", "react-native-level-fs": "3.0.1",
"react-native-linear-gradient": "2.5.4", "react-native-linear-gradient": "2.5.6",
"react-native-modal": "11.1.0", "react-native-modal": "11.5.3",
"react-native-obscure": "1.2.1", "react-native-obscure": "1.2.1",
"react-native-popup-menu-android": "1.0.3", "react-native-popup-menu-android": "1.0.3",
"react-native-privacy-snapshot": "git+https://github.com/BlueWallet/react-native-privacy-snapshot.git", "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-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-quick-actions": "0.3.13",
"react-native-randombytes": "3.5.3", "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-secure-key-store": "git+https://github.com/marcosrdz/react-native-secure-key-store.git#38332f629f577cdd57c69fc8cc971b3cbad193c9",
"react-native-share": "2.0.0", "react-native-share": "2.0.0",
"react-native-snap-carousel": "3.8.4", "react-native-snap-carousel": "3.8.4",
"react-native-sortable-list": "0.0.23", "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-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-tcp": "git+https://github.com/aprock/react-native-tcp.git",
"react-native-tooltip": "git+https://github.com/marcosrdz/react-native-tooltip.git", "react-native-tooltip": "git+https://github.com/marcosrdz/react-native-tooltip.git",
"react-native-vector-icons": "6.6.0", "react-native-vector-icons": "6.6.0",
"react-native-watch-connectivity": "0.4.2", "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": "3.11.0",
"react-navigation-hooks": "1.1.0", "react-navigation-hooks": "1.1.0",
"react-test-render": "1.1.2", "react-test-render": "1.1.2",
"readable-stream": "3.4.0", "readable-stream": "3.6.0",
"secure-random": "1.1.2", "secure-random": "1.1.2",
"stream-browserify": "2.0.2", "stream-browserify": "2.0.2",
"url": "0.11.0", "url": "0.11.0",

5
screen/lnd/lndCreateInvoice.js

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

3
screen/receive/details.js

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

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 DocumentPicker from 'react-native-document-picker';
import RNFS from 'react-native-fs'; import RNFS from 'react-native-fs';
const LocalQRCode = require('@remobile/react-native-qrcode-local-image'); const LocalQRCode = require('@remobile/react-native-qrcode-local-image');
const createHash = require('create-hash');
const ScanQRCode = ({ const ScanQRCode = ({
onBarScanned = useNavigationParam('onBarScanned'), onBarScanned = useNavigationParam('onBarScanned'),
@ -21,7 +22,23 @@ const ScanQRCode = ({
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { navigate, goBack } = useNavigation(); const { navigate, goBack } = useNavigation();
const scannedCache = {};
const HashIt = function(s) {
return createHash('sha256')
.update(s)
.digest()
.toString('hex');
};
const onBarCodeRead = ret => { 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) { if (!isLoading && !cameraPreviewIsPaused) {
setIsLoading(true); setIsLoading(true);
try { try {
@ -51,7 +68,7 @@ const ScanQRCode = ({
if (fileParsed.keystore.ckcc_xfp) { if (fileParsed.keystore.ckcc_xfp) {
masterFingerprint = Number(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 { } else {
throw new Error(); throw new Error();
} }

8
screen/settings/about.js

@ -121,12 +121,10 @@ const About = () => {
<BlueText h3>Built with awesome:</BlueText> <BlueText h3>Built with awesome:</BlueText>
<BlueSpacing20 /> <BlueSpacing20 />
<BlueText h4>* React Native</BlueText> <BlueText h4>* React Native</BlueText>
<BlueText h4>* Bitcoinjs-lib</BlueText> <BlueText h4>* bitcoinjs-lib</BlueText>
<BlueText h4>* blockcypher.com API</BlueText>
<BlueText h4>* Nodejs</BlueText> <BlueText h4>* Nodejs</BlueText>
<BlueText h4>* react-native-elements</BlueText> <BlueText h4>* Electrum server</BlueText>
<BlueText h4>* rn-nodeify</BlueText> <BlueSpacing20 />
<BlueText h4>* bignumber.js</BlueText>
<BlueSpacing20 /> <BlueSpacing20 />
<BlueButton onPress={handleOnReleaseNotesPress} title="Release notes" /> <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, BlueNavigationStyle,
} from '../../BlueComponents'; } from '../../BlueComponents';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { HDSegwitBech32Transaction, HDSegwitBech32Wallet } from '../../class'; import { HDSegwitBech32Transaction } from '../../class';
import { BitcoinUnit } from '../../models/bitcoinUnits'; import { BitcoinUnit } from '../../models/bitcoinUnits';
import { Icon } from 'react-native-elements'; import { Icon } from 'react-native-elements';
import Handoff from 'react-native-handoff'; import Handoff from 'react-native-handoff';
@ -93,7 +93,7 @@ export default class TransactionsStatus extends Component {
} }
async checkPossibilityOfCPFP() { async checkPossibilityOfCPFP() {
if (this.state.wallet.type !== HDSegwitBech32Wallet.type) { if (!this.state.wallet.allowRBF()) {
return this.setState({ isCPFPpossible: buttonStatus.notPossible }); return this.setState({ isCPFPpossible: buttonStatus.notPossible });
} }
@ -106,7 +106,7 @@ export default class TransactionsStatus extends Component {
} }
async checkPossibilityOfRBFBumpFee() { async checkPossibilityOfRBFBumpFee() {
if (this.state.wallet.type !== HDSegwitBech32Wallet.type) { if (!this.state.wallet.allowRBF()) {
return this.setState({ isRBFBumpFeePossible: buttonStatus.notPossible }); return this.setState({ isRBFBumpFeePossible: buttonStatus.notPossible });
} }
@ -119,7 +119,7 @@ export default class TransactionsStatus extends Component {
} }
async checkPossibilityOfRBFCancel() { async checkPossibilityOfRBFCancel() {
if (this.state.wallet.type !== HDSegwitBech32Wallet.type) { if (!this.state.wallet.allowRBF()) {
return this.setState({ isRBFCancelPossible: buttonStatus.notPossible }); return this.setState({ isRBFCancelPossible: buttonStatus.notPossible });
} }
@ -250,24 +250,6 @@ export default class TransactionsStatus extends Component {
</BlueCard> </BlueCard>
<View style={{ alignSelf: 'center', justifyContent: 'center' }}> <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) { if (this.state.isCPFPpossible === buttonStatus.unknown) {
return ( 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 }}> <SafeBlueArea forceInset={{ horizontal: 'always' }} style={{ flex: 1, paddingTop: 40 }}>
<ScrollView> <ScrollView>
<BlueFormLabel>{loc.wallets.add.wallet_name}</BlueFormLabel> <BlueFormLabel>{loc.wallets.add.wallet_name}</BlueFormLabel>
<KeyboardAvoidingView <KeyboardAvoidingView enabled behavior={Platform.OS === 'ios' ? 'position' : null} keyboardVerticalOffset={20}>
style={{ <View
flexDirection: 'row', style={{
borderColor: '#d2d2d2', flexDirection: 'row',
borderBottomColor: '#d2d2d2', borderColor: '#d2d2d2',
borderWidth: 1.0, borderBottomColor: '#d2d2d2',
borderBottomWidth: 0.5, borderWidth: 1.0,
backgroundColor: '#f5f5f5', borderBottomWidth: 0.5,
minHeight: 44, backgroundColor: '#f5f5f5',
height: 44, minHeight: 44,
marginHorizontal: 20, height: 44,
alignItems: 'center', marginHorizontal: 20,
marginVertical: 16, alignItems: 'center',
borderRadius: 4, 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);
}} }}
style={{ flex: 1, marginHorizontal: 8, color: '#81868e' }} >
editable={!this.state.isLoading} <TextInput
underlineColorAndroid="transparent" 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> </KeyboardAvoidingView>
<BlueFormLabel>{loc.wallets.add.wallet_type}</BlueFormLabel> <BlueFormLabel>{loc.wallets.add.wallet_type}</BlueFormLabel>

3
screen/wallets/details.js

@ -218,8 +218,7 @@ export default class WalletDetails extends Component {
<BlueButton <BlueButton
onPress={() => onPress={() =>
this.props.navigation.navigate('WalletExport', { this.props.navigation.navigate('WalletExport', {
address: this.state.wallet.getAddress(), wallet: this.state.wallet,
secret: this.state.wallet.getSecret(),
}) })
} }
title={loc.wallets.details.export_backup} 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 PropTypes from 'prop-types';
import Privacy from '../../Privacy'; import Privacy from '../../Privacy';
import Biometric from '../../class/biometrics'; import Biometric from '../../class/biometrics';
import { LightningCustodianWallet } from '../../class'; import { LegacyWallet, LightningCustodianWallet, SegwitBech32Wallet, SegwitP2SHWallet } from '../../class';
/** @type {AppStorage} */ /** @type {AppStorage} */
let BlueApp = require('../../BlueApp'); let BlueApp = require('../../BlueApp');
let loc = require('../../loc'); let loc = require('../../loc');
@ -20,17 +20,7 @@ export default class WalletExport extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
let wallet = props.navigation.state.params.wallet;
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;
}
}
this.state = { this.state = {
isLoading: true, isLoading: true,
qrCodeHeight: height > width ? width - 40 : width / 2, qrCodeHeight: height > width ? width - 40 : width / 2,
@ -89,7 +79,7 @@ export default class WalletExport extends Component {
</View> </View>
{(() => { {(() => {
if (this.state.wallet.getAddress()) { if ([LegacyWallet.type, SegwitBech32Wallet.type, SegwitP2SHWallet.type].includes(this.state.wallet.type)) {
return ( return (
<BlueCard> <BlueCard>
<BlueText>{this.state.wallet.getAddress()}</BlueText> <BlueText>{this.state.wallet.getAddress()}</BlueText>
@ -125,8 +115,7 @@ WalletExport.propTypes = {
navigation: PropTypes.shape({ navigation: PropTypes.shape({
state: PropTypes.shape({ state: PropTypes.shape({
params: PropTypes.shape({ params: PropTypes.shape({
address: PropTypes.string, wallet: PropTypes.object.isRequired,
secret: PropTypes.string,
}), }),
}), }),
navigate: PropTypes.func, navigate: PropTypes.func,

2
screen/wallets/import.js

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

2
screen/wallets/list.js

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

3
screen/wallets/transactions.js

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

110
tests/integration/App.test.js

@ -1,6 +1,6 @@
/* global describe, it, expect, jest, jasmine */ /* global describe, it, expect, jest, jasmine */
import React from 'react'; import React from 'react';
import { LegacyWallet, SegwitP2SHWallet, AppStorage } from '../../class'; import { AppStorage } from '../../class';
import TestRenderer from 'react-test-renderer'; import TestRenderer from 'react-test-renderer';
import Settings from '../../screen/settings/settings'; import Settings from '../../screen/settings/settings';
import Selftest from '../../screen/selftest'; import Selftest from '../../screen/selftest';
@ -49,28 +49,6 @@ jest.mock('ScrollView', () => {
return 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', () => { it('BlueHeader works', () => {
const rendered = TestRenderer.create(<BlueHeader />).toJSON(); const rendered = TestRenderer.create(<BlueHeader />).toJSON();
expect(rendered).toBeTruthy(); expect(rendered).toBeTruthy();
@ -105,92 +83,6 @@ it('Selftest work', () => {
assert.ok(okFound, 'OK not found. Got: ' + allTests.join('; ')); 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', () => { describe('currency', () => {
it('fetches exchange rate and saves to AsyncStorage', async () => { it('fetches exchange rate and saves to AsyncStorage', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; 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'); 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) { if (!process.env.HD_MNEMONIC) {
console.error('process.env.HD_MNEMONIC not set, skipped'); console.error('process.env.HD_MNEMONIC not set, skipped');
return; return;
@ -315,15 +315,14 @@ it.skip('Legacy HD (BIP44) can create TX', async () => {
hd.setSecret(process.env.HD_MNEMONIC); hd.setSecret(process.env.HD_MNEMONIC);
assert.ok(hd.validateMnemonic()); assert.ok(hd.validateMnemonic());
await hd.fetchBalance();
await hd.fetchUtxo(); await hd.fetchUtxo();
assert.strictEqual(hd.utxo.length, 4); 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'); let txhex = hd.createTx(hd.utxo, 0.0008, 0.000005, '3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK');
assert.strictEqual( assert.strictEqual(
txhex, txhex,
'01000000045fbc74110c2d6fcf4d1161a59913fbcd2b6ab3c5a9eb4d0dc0859515cbc8654f030000006b4830450221009be5dbe37db5a8409ddce3570140c95d162a07651b1e48cf39a6a741892adc53022061a25b8024d8f3cb1b94f264245de0c6e9a103ea557ddeb66245b40ec8e9384b012102ad7b2216f3a2b38d56db8a7ee5c540fd12c4bbb7013106eff78cc2ace65aa002ffffffff5fbc74110c2d6fcf4d1161a59913fbcd2b6ab3c5a9eb4d0dc0859515cbc8654f000000006a47304402207106e9fa4e2e35d351fbccc9c0fad3356d85d0cd35a9d7e9cbcefce5440da1e5022073c1905b5927447378c0f660e62900c1d4b2691730799458889fb87d86f5159101210316e84a2556f30a199541633f5dda6787710ccab26771b7084f4c9e1104f47667ffffffff5fbc74110c2d6fcf4d1161a59913fbcd2b6ab3c5a9eb4d0dc0859515cbc8654f020000006a4730440220250b15094096c4d4fe6793da8e45fa118ed057cc2759a480c115e76e23590791022079cdbdc9e630d713395602071e2837ecc1d192a36a24d8ec71bc51d5e62b203b01210316e84a2556f30a199541633f5dda6787710ccab26771b7084f4c9e1104f47667ffffffff5fbc74110c2d6fcf4d1161a59913fbcd2b6ab3c5a9eb4d0dc0859515cbc8654f010000006b483045022100879da610e6ed12c84d55f12baf3bf6222d59b5282502b3c7f4db1d22152c16900220759a1c88583cbdaf7fde21c273ad985dfdf94a2fa85e42ee41dcea2fd69136fd012102ad7b2216f3a2b38d56db8a7ee5c540fd12c4bbb7013106eff78cc2ace65aa002ffffffff02803801000000000017a914a3a65daca3064280ae072b9d6773c027b30abace872c4c0000000000001976a9146ee5e3e66dc73587a3a2d77a1a6c8554fae21b8a88ac00000000', '01000000045fbc74110c2d6fcf4d1161a59913fbcd2b6ab3c5a9eb4d0dc0859515cbc8654f000000006b48304502210080ffbde0d510c3fb9abcc5f7570448e9c0f7138d0b355d00bb97cada0679ac9502207ffd205373829c800ec08079a4280c3abe6f6f8c94ae7af0157a14ea5629d28701210316e84a2556f30a199541633f5dda6787710ccab26771b7084f4c9e1104f47667ffffffff5fbc74110c2d6fcf4d1161a59913fbcd2b6ab3c5a9eb4d0dc0859515cbc8654f010000006a473044022077788d7e118802fd7268aac7a1dde5a6724f01936e23edd46ac2750fd39265be0220776ac9e4c285580d06510a00b561cec6de1813293e7b04b6f870138af832bf9e012102ad7b2216f3a2b38d56db8a7ee5c540fd12c4bbb7013106eff78cc2ace65aa002ffffffff5fbc74110c2d6fcf4d1161a59913fbcd2b6ab3c5a9eb4d0dc0859515cbc8654f020000006b4830450221009e47b48dd1eee6d00a1817480605f446e11949b1e6f464f43f04bce2fc787ea9022022b3dcf80e7b2c995cf6defb3425b57d8a80918c7f543faaa0497d853820779101210316e84a2556f30a199541633f5dda6787710ccab26771b7084f4c9e1104f47667ffffffff5fbc74110c2d6fcf4d1161a59913fbcd2b6ab3c5a9eb4d0dc0859515cbc8654f030000006b48304502210089c20d6c0f6486c5979cf69a3c849f09e36416e5604499c05ae2dc22bea8553d022011241a206d550e55b4476ac5ba0fd744f0965d8f8bd69a740e428770689749a1012102ad7b2216f3a2b38d56db8a7ee5c540fd12c4bbb7013106eff78cc2ace65aa002ffffffff02803801000000000017a914a3a65daca3064280ae072b9d6773c027b30abace872c4c0000000000001976a9146ee5e3e66dc73587a3a2d77a1a6c8554fae21b8a88ac00000000',
); );
var tx = bitcoin.Transaction.fromHex(txhex); 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); assert.strictEqual(tx.outs[0].value, 99800);
}); });
it('Legacy HD (BIP44) can fetch UTXO', async function() { it('HD breadwallet works', 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() {
if (!process.env.HD_MNEMONIC_BREAD) { if (!process.env.HD_MNEMONIC_BREAD) {
console.error('process.env.HD_MNEMONIC_BREAD not set, skipped'); console.error('process.env.HD_MNEMONIC_BREAD not set, skipped');
return; return;
@ -379,17 +363,17 @@ it.skip('HD breadwallet works', async function() {
'xpub68nLLEi3KERQY7jyznC9PQSpSjmekrEmN8324YRCXayMXaavbdEJsK4gEcX2bNf9vGzT4xRks9utZ7ot1CTHLtdyCn9udvv1NWvtY7HXroh', 'xpub68nLLEi3KERQY7jyznC9PQSpSjmekrEmN8324YRCXayMXaavbdEJsK4gEcX2bNf9vGzT4xRks9utZ7ot1CTHLtdyCn9udvv1NWvtY7HXroh',
); );
await hdBread.fetchBalance(); await hdBread.fetchBalance();
assert.strictEqual(hdBread.balance, 0); assert.strictEqual(hdBread.getBalance(), 123456);
assert.ok(hdBread._lastTxFetch === 0); assert.ok(hdBread._lastTxFetch === 0);
await hdBread.fetchTransactions(); await hdBread.fetchTransactions();
assert.ok(hdBread._lastTxFetch > 0); assert.ok(hdBread._lastTxFetch > 0);
assert.strictEqual(hdBread.getTransactions().length, 177); assert.strictEqual(hdBread.getTransactions().length, 178);
for (let tx of hdBread.getTransactions()) { for (let tx of hdBread.getTransactions()) {
assert.ok(tx.confirmations); 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); assert.strictEqual(hdBread.next_free_change_address_index, 118);
// checking that internal pointer and async address getter return the same address // 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(); await BlueElectrum.waitTillConnected();
}); });
jasmine.DEFAULT_TIMEOUT_INTERVAL = 500 * 1000;
describe('Watch only wallet', () => { describe('Watch only wallet', () => {
it('can fetch balance', async () => { it('can fetch balance', async () => {
let w = new WatchOnlyWallet(); let w = new WatchOnlyWallet();
@ -25,12 +27,11 @@ describe('Watch only wallet', () => {
}); });
it('can fetch tx', async () => { it('can fetch tx', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 150 * 1000;
let w = new WatchOnlyWallet(); let w = new WatchOnlyWallet();
w.setSecret('167zK5iZrs1U6piDqubD3FjRqUTM2CZnb8'); w.setSecret('167zK5iZrs1U6piDqubD3FjRqUTM2CZnb8');
await w.fetchTransactions(); 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 = new WatchOnlyWallet();
w.setSecret('1BiJW1jyUaxcJp2JWwbPLPzB1toPNWTFJV'); w.setSecret('1BiJW1jyUaxcJp2JWwbPLPzB1toPNWTFJV');
@ -42,8 +43,33 @@ describe('Watch only wallet', () => {
assert.strictEqual(w.getTransactions().length, 2); 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 () => { it('can fetch complex TXs', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 120 * 1000;
let w = new WatchOnlyWallet(); let w = new WatchOnlyWallet();
w.setSecret('3NLnALo49CFEF4tCRhCvz45ySSfz3UktZC'); w.setSecret('3NLnALo49CFEF4tCRhCvz45ySSfz3UktZC');
await w.fetchTransactions(); await w.fetchTransactions();
@ -54,28 +80,34 @@ describe('Watch only wallet', () => {
it('can validate address', async () => { it('can validate address', async () => {
let w = new WatchOnlyWallet(); let w = new WatchOnlyWallet();
w.setSecret('12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG'); for (let secret of [
assert.ok(w.valid()); 'bc1quhnve8q4tk3unhmjts7ymxv8cd6w9xv8wy29uv',
assert.strictEqual(w.isHd(), false); '12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG',
w.setSecret('3BDsBDxDimYgNZzsqszNZobqQq3yeUoJf2'); '3BDsBDxDimYgNZzsqszNZobqQq3yeUoJf2',
assert.ok(w.valid()); 'BC1QUHNVE8Q4TK3UNHMJTS7YMXV8CD6W9XV8WY29UV',
assert.strictEqual(w.isHd(), false); ]) {
w.setSecret(secret);
assert.ok(w.valid());
assert.strictEqual(w.isHd(), false);
}
w.setSecret('not valid'); w.setSecret('not valid');
assert.ok(!w.valid()); assert.ok(!w.valid());
w.setSecret('xpub6CQdfC3v9gU86eaSn7AhUFcBVxiGhdtYxdC5Cw2vLmFkfth2KXCMmYcPpvZviA89X6DXDs4PJDk5QVL2G2xaVjv7SM4roWHr1gR4xB3Z7Ps'); for (let secret of [
assert.ok(w.valid()); 'xpub6CQdfC3v9gU86eaSn7AhUFcBVxiGhdtYxdC5Cw2vLmFkfth2KXCMmYcPpvZviA89X6DXDs4PJDk5QVL2G2xaVjv7SM4roWHr1gR4xB3Z7Ps',
w.setSecret('ypub6XRzrn3HB1tjhhvrHbk1vnXCecZEdXohGzCk3GXwwbDoJ3VBzZ34jNGWbC6WrS7idXrYjjXEzcPDX5VqnHEnuNf5VAXgLfSaytMkJ2rwVqy'); 'ypub6XRzrn3HB1tjhhvrHbk1vnXCecZEdXohGzCk3GXwwbDoJ3VBzZ34jNGWbC6WrS7idXrYjjXEzcPDX5VqnHEnuNf5VAXgLfSaytMkJ2rwVqy',
assert.ok(w.valid()); 'zpub6r7jhKKm7BAVx3b3nSnuadY1WnshZYkhK8gKFoRLwK9rF3Mzv28BrGcCGA3ugGtawi1WLb2vyjQAX9ZTDGU5gNk2bLdTc3iEXr6tzR1ipNP',
w.setSecret('zpub6r7jhKKm7BAVx3b3nSnuadY1WnshZYkhK8gKFoRLwK9rF3Mzv28BrGcCGA3ugGtawi1WLb2vyjQAX9ZTDGU5gNk2bLdTc3iEXr6tzR1ipNP'); ]) {
assert.ok(w.valid()); w.setSecret(secret);
assert.strictEqual(w.isHd(), true); assert.ok(w.valid());
assert.strictEqual(w.getMasterFingerprint(), false); assert.strictEqual(w.isHd(), true);
assert.strictEqual(w.getMasterFingerprintHex(), '00000000'); assert.strictEqual(w.getMasterFingerprint(), false);
assert.strictEqual(w.getMasterFingerprintHex(), '00000000');
}
}); });
it('can fetch balance & transactions from zpub HD', async () => { it('can fetch balance & transactions from zpub HD', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000;
let w = new WatchOnlyWallet(); let w = new WatchOnlyWallet();
w.setSecret('zpub6r7jhKKm7BAVx3b3nSnuadY1WnshZYkhK8gKFoRLwK9rF3Mzv28BrGcCGA3ugGtawi1WLb2vyjQAX9ZTDGU5gNk2bLdTc3iEXr6tzR1ipNP'); w.setSecret('zpub6r7jhKKm7BAVx3b3nSnuadY1WnshZYkhK8gKFoRLwK9rF3Mzv28BrGcCGA3ugGtawi1WLb2vyjQAX9ZTDGU5gNk2bLdTc3iEXr6tzR1ipNP');
await w.fetchBalance(); 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 () => { 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 = 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}'; '{"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(); let w = new WatchOnlyWallet();
@ -175,7 +206,6 @@ describe('Watch only wallet', () => {
}); });
it('can fetch balance & transactions from ypub HD', async () => { it('can fetch balance & transactions from ypub HD', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000;
let w = new WatchOnlyWallet(); let w = new WatchOnlyWallet();
w.setSecret('ypub6Y9u3QCRC1HkZv3stNxcQVwmw7vC7KX5Ldz38En5P88RQbesP2oy16hNyQocVCfYRQPxdHcd3pmu9AFhLv7NdChWmw5iNLryZ2U6EEHdnfo'); w.setSecret('ypub6Y9u3QCRC1HkZv3stNxcQVwmw7vC7KX5Ldz38En5P88RQbesP2oy16hNyQocVCfYRQPxdHcd3pmu9AFhLv7NdChWmw5iNLryZ2U6EEHdnfo');
await w.fetchBalance(); await w.fetchBalance();
@ -186,7 +216,6 @@ describe('Watch only wallet', () => {
}); });
it('can fetch balance & transactions from xpub HD', async () => { it('can fetch balance & transactions from xpub HD', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000;
let w = new WatchOnlyWallet(); let w = new WatchOnlyWallet();
w.setSecret('xpub6CQdfC3v9gU86eaSn7AhUFcBVxiGhdtYxdC5Cw2vLmFkfth2KXCMmYcPpvZviA89X6DXDs4PJDk5QVL2G2xaVjv7SM4roWHr1gR4xB3Z7Ps'); w.setSecret('xpub6CQdfC3v9gU86eaSn7AhUFcBVxiGhdtYxdC5Cw2vLmFkfth2KXCMmYcPpvZviA89X6DXDs4PJDk5QVL2G2xaVjv7SM4roWHr1gR4xB3Z7Ps');
await w.fetchBalance(); await w.fetchBalance();
@ -197,7 +226,6 @@ describe('Watch only wallet', () => {
}); });
it('can fetch large HD', async () => { it('can fetch large HD', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 500 * 1000;
let w = new WatchOnlyWallet(); let w = new WatchOnlyWallet();
w.setSecret('ypub6WnnYxkQCGeowv4BXq9Y9PHaXgHMJg9TkFaDJkunhcTAfbDw8z3LvV9kFNHGjeVaEoGdsSJgaMWpUBvYvpYGMJd43gTK5opecVVkvLwKttx'); w.setSecret('ypub6WnnYxkQCGeowv4BXq9Y9PHaXgHMJg9TkFaDJkunhcTAfbDw8z3LvV9kFNHGjeVaEoGdsSJgaMWpUBvYvpYGMJd43gTK5opecVVkvLwKttx');
await w.fetchBalance(); await w.fetchBalance();

Loading…
Cancel
Save