Merge branch 'master' of github.com:BlueWallet/BlueWallet

This commit is contained in:
Overtorment 2019-05-06 15:07:42 +01:00
commit f44f86828e
14 changed files with 576 additions and 33 deletions

View file

@ -1,4 +1,5 @@
import AsyncStorage from '@react-native-community/async-storage';
import { SegwitBech32Wallet } from './class';
const ElectrumClient = require('electrum-client');
let bitcoin = require('bitcoinjs-lib');
let reverse = require('buffer-reverse');
@ -19,7 +20,6 @@ const hardcodedPeers = [
{ host: 'electrum2.bluewallet.io', tcp: '50001' },
{ host: 'electrum3.bluewallet.io', tcp: '50001' },
{ host: 'electrum3.bluewallet.io', tcp: '50001' }, // 2x weight
{ host: 'electrum.coinop.cc', tcp: '50001' },
];
let mainClient = false;
@ -123,23 +123,47 @@ async function getTransactionsByAddress(address) {
return history;
}
async function getTransactionsFullByAddress(address) {
let txs = await this.getTransactionsByAddress(address);
let ret = [];
for (let tx of txs) {
let full = await mainClient.blockchainTransaction_get(tx.tx_hash, true);
full.address = address;
for (let vin of full.vin) {
vin.address = SegwitBech32Wallet.witnessToAddress(vin.txinwitness[1]);
// now we need to fetch previous TX where this VIN became an output, so we can see its amount
let prevTxForVin = await mainClient.blockchainTransaction_get(vin.txid, true);
if (prevTxForVin && prevTxForVin.vout && prevTxForVin.vout[vin.vout]) {
vin.value = prevTxForVin.vout[vin.vout].value;
}
}
delete full.hex; // compact
delete full.hash; // compact
ret.push(full);
}
return ret;
}
/**
*
* @param addresses {Array}
* @returns {Promise<{balance: number, unconfirmed_balance: number}>}
* @returns {Promise<{balance: number, unconfirmed_balance: number, addresses: object}>}
*/
async function multiGetBalanceByAddress(addresses) {
if (!mainClient) throw new Error('Electrum client is not connected');
let balance = 0;
let unconfirmedBalance = 0;
let addressesAssoc = {};
for (let addr of addresses) {
let b = await getBalanceByAddress(addr);
balance += b.confirmed;
unconfirmedBalance += b.unconfirmed_balance;
unconfirmedBalance += b.unconfirmed;
addressesAssoc[addr] = b;
}
return { balance, unconfirmed_balance: unconfirmedBalance };
return { balance, unconfirmed_balance: unconfirmedBalance, addresses: addressesAssoc };
}
/**
@ -187,6 +211,7 @@ async function broadcast(hex) {
module.exports.getBalanceByAddress = getBalanceByAddress;
module.exports.getTransactionsByAddress = getTransactionsByAddress;
module.exports.multiGetBalanceByAddress = multiGetBalanceByAddress;
module.exports.getTransactionsFullByAddress = getTransactionsFullByAddress;
module.exports.waitTillConnected = waitTillConnected;
module.exports.estimateFees = estimateFees;
module.exports.broadcast = broadcast;

View file

@ -6,7 +6,8 @@ jasmine.DEFAULT_TIMEOUT_INTERVAL = 150 * 1000;
afterAll(() => {
// after all tests we close socket so the test suite can actually terminate
return BlueElectrum.forceDisconnect();
BlueElectrum.forceDisconnect();
return new Promise(resolve => setTimeout(resolve, 10000)); // simple sleep to wait for all timeouts termination
});
beforeAll(async () => {
@ -60,18 +61,51 @@ describe('Electrum', () => {
}
});
it('BlueElectrum works', async function() {
it('BlueElectrum can do getBalanceByAddress()', async function() {
let address = '3GCvDBAktgQQtsbN6x5DYiQCMmgZ9Yk8BK';
let balance = await BlueElectrum.getBalanceByAddress(address);
assert.strictEqual(balance.confirmed, 51432);
assert.strictEqual(balance.unconfirmed, 0);
assert.strictEqual(balance.addr, address);
});
let txs = await BlueElectrum.getTransactionsByAddress(address);
it('BlueElectrum can do getTransactionsByAddress()', async function() {
let txs = await BlueElectrum.getTransactionsByAddress('bc1qt4t9xl2gmjvxgmp5gev6m8e6s9c85979ta7jeh');
assert.strictEqual(txs.length, 1);
assert.strictEqual(txs[0].tx_hash, 'ad00a92409d8982a1d7f877056dbed0c4337d2ebab70b30463e2802279fb936d');
assert.strictEqual(txs[0].height, 563077);
});
it.only('BlueElectrum can do getTransactionsFullByAddress()', async function() {
let txs = await BlueElectrum.getTransactionsFullByAddress('bc1qt4t9xl2gmjvxgmp5gev6m8e6s9c85979ta7jeh');
for (let tx of txs) {
assert.ok(tx.tx_hash);
assert.ok(tx.height);
assert.ok(tx.address === 'bc1qt4t9xl2gmjvxgmp5gev6m8e6s9c85979ta7jeh');
assert.ok(tx.txid);
assert.ok(tx.confirmations);
assert.ok(tx.vin);
assert.ok(tx.vout);
assert.ok(tx.vout[0].value);
assert.ok(tx.vout[0].scriptPubKey);
}
});
it('BlueElectrum can do multiGetBalanceByAddress()', async function() {
let balances = await BlueElectrum.multiGetBalanceByAddress([
'bc1qt4t9xl2gmjvxgmp5gev6m8e6s9c85979ta7jeh',
'bc1qvd6w54sydc08z3802svkxr7297ez7cusd6266p',
'bc1qwp58x4c9e5cplsnw5096qzdkae036ug7a34x3r',
'bc1qcg6e26vtzja0h8up5w2m7utex0fsu4v0e0e7uy',
]);
assert.strictEqual(balances.balance, 200000);
assert.strictEqual(balances.unconfirmed_balance, 0);
assert.strictEqual(balances.addresses['bc1qt4t9xl2gmjvxgmp5gev6m8e6s9c85979ta7jeh'].confirmed, 50000);
assert.strictEqual(balances.addresses['bc1qt4t9xl2gmjvxgmp5gev6m8e6s9c85979ta7jeh'].unconfirmed, 0);
assert.strictEqual(balances.addresses['bc1qvd6w54sydc08z3802svkxr7297ez7cusd6266p'].confirmed, 50000);
assert.strictEqual(balances.addresses['bc1qvd6w54sydc08z3802svkxr7297ez7cusd6266p'].unconfirmed, 0);
assert.strictEqual(balances.addresses['bc1qwp58x4c9e5cplsnw5096qzdkae036ug7a34x3r'].confirmed, 50000);
assert.strictEqual(balances.addresses['bc1qwp58x4c9e5cplsnw5096qzdkae036ug7a34x3r'].unconfirmed, 0);
assert.strictEqual(balances.addresses['bc1qcg6e26vtzja0h8up5w2m7utex0fsu4v0e0e7uy'].confirmed, 50000);
assert.strictEqual(balances.addresses['bc1qcg6e26vtzja0h8up5w2m7utex0fsu4v0e0e7uy'].unconfirmed, 0);
});
});

145
HDBech32Wallet.test.js Normal file
View file

@ -0,0 +1,145 @@
/* global it, describe, jasmine, afterAll, beforeAll */
import { HDSegwitBech32Wallet } from './class';
global.crypto = require('crypto'); // shall be used by tests under nodejs CLI, but not in RN environment
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
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();
console.log('electrum connected');
});
describe('Bech32 Segwit HD (BIP84)', () => {
it('can create', async function() {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 30 * 1000;
let mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
let hd = new HDSegwitBech32Wallet();
hd.setSecret(mnemonic);
assert.strictEqual(true, hd.validateMnemonic());
assert.strictEqual(
'zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs',
hd.getXpub(),
);
assert.strictEqual(hd._getExternalWIFByIndex(0), 'KyZpNDKnfs94vbrwhJneDi77V6jF64PWPF8x5cdJb8ifgg2DUc9d');
assert.strictEqual(hd._getExternalWIFByIndex(1), 'Kxpf5b8p3qX56DKEe5NqWbNUP9MnqoRFzZwHRtsFqhzuvUJsYZCy');
assert.strictEqual(hd._getInternalWIFByIndex(0), 'KxuoxufJL5csa1Wieb2kp29VNdn92Us8CoaUG3aGtPtcF3AzeXvF');
assert.ok(hd._getInternalWIFByIndex(0) !== hd._getInternalWIFByIndex(1));
assert.strictEqual(hd._getExternalAddressByIndex(0), 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu');
assert.strictEqual(hd._getExternalAddressByIndex(1), 'bc1qnjg0jd8228aq7egyzacy8cys3knf9xvrerkf9g');
assert.strictEqual(hd._getInternalAddressByIndex(0), 'bc1q8c6fshw2dlwun7ekn9qwf37cu2rn755upcp6el');
assert.ok(hd._getInternalAddressByIndex(0) !== hd._getInternalAddressByIndex(1));
assert.ok(hd._lastBalanceFetch === 0);
await hd.fetchBalance();
assert.strictEqual(hd.getBalance(), 0);
assert.ok(hd._lastBalanceFetch > 0);
// checking that internal pointer and async address getter return the same address
let freeAddress = await hd.getAddressAsync();
assert.strictEqual(hd.next_free_address_index, 0);
assert.strictEqual(hd._getExternalAddressByIndex(hd.next_free_address_index), freeAddress);
let freeChangeAddress = await hd.getChangeAddressAsync();
assert.strictEqual(hd.next_free_change_address_index, 0);
assert.strictEqual(hd._getInternalAddressByIndex(hd.next_free_change_address_index), freeChangeAddress);
});
it('can fetch balance', async function() {
if (!process.env.HD_MNEMONIC) {
console.error('process.env.HD_MNEMONIC not set, skipped');
return;
}
jasmine.DEFAULT_TIMEOUT_INTERVAL = 90 * 1000;
let hd = new HDSegwitBech32Wallet();
hd.setSecret(process.env.HD_MNEMONIC);
assert.ok(hd.validateMnemonic());
assert.strictEqual(
'zpub6r7jhKKm7BAVx3b3nSnuadY1WnshZYkhK8gKFoRLwK9rF3Mzv28BrGcCGA3ugGtawi1WLb2vyjQAX9ZTDGU5gNk2bLdTc3iEXr6tzR1ipNP',
hd.getXpub(),
);
assert.strictEqual(hd._getExternalAddressByIndex(0), 'bc1qvd6w54sydc08z3802svkxr7297ez7cusd6266p');
assert.strictEqual(hd._getExternalAddressByIndex(1), 'bc1qt4t9xl2gmjvxgmp5gev6m8e6s9c85979ta7jeh');
assert.strictEqual(hd._getInternalAddressByIndex(0), 'bc1qcg6e26vtzja0h8up5w2m7utex0fsu4v0e0e7uy');
assert.strictEqual(hd._getInternalAddressByIndex(1), 'bc1qwp58x4c9e5cplsnw5096qzdkae036ug7a34x3r');
await hd.fetchBalance();
assert.strictEqual(hd.getBalance(), 200000);
assert.strictEqual(await hd.getAddressAsync(), hd._getExternalAddressByIndex(2));
assert.strictEqual(await hd.getChangeAddressAsync(), hd._getInternalAddressByIndex(2));
assert.strictEqual(hd.next_free_address_index, 2);
assert.strictEqual(hd.next_free_change_address_index, 2);
// now, reset HD wallet, and find free addresses from scratch:
hd = new HDSegwitBech32Wallet();
hd.setSecret(process.env.HD_MNEMONIC);
assert.strictEqual(await hd.getAddressAsync(), hd._getExternalAddressByIndex(2));
assert.strictEqual(await hd.getChangeAddressAsync(), hd._getInternalAddressByIndex(2));
assert.strictEqual(hd.next_free_address_index, 2);
assert.strictEqual(hd.next_free_change_address_index, 2);
});
it('can fetch transactions', async function() {
if (!process.env.HD_MNEMONIC) {
console.error('process.env.HD_MNEMONIC not set, skipped');
return;
}
jasmine.DEFAULT_TIMEOUT_INTERVAL = 90 * 1000;
let hd = new HDSegwitBech32Wallet();
hd.setSecret(process.env.HD_MNEMONIC);
assert.ok(hd.validateMnemonic());
await hd.fetchBalance();
await hd.fetchTransactions();
assert.strictEqual(hd.getTransactions().length, 4);
// console.warn(JSON.stringify(hd.getTransactions(), null, 2));
for (let tx of hd.getTransactions()) {
assert.ok(tx.hash);
assert.strictEqual(tx.value, 50000);
assert.ok(tx.timestamp);
assert.ok(tx.confirmations > 1);
}
});
it('can generate addresses only via zpub', function() {
let zpub = 'zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs';
let hd = new HDSegwitBech32Wallet();
hd._xpub = zpub;
assert.strictEqual(hd._getExternalAddressByIndex(0), 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu');
assert.strictEqual(hd._getExternalAddressByIndex(1), 'bc1qnjg0jd8228aq7egyzacy8cys3knf9xvrerkf9g');
assert.strictEqual(hd._getInternalAddressByIndex(0), 'bc1q8c6fshw2dlwun7ekn9qwf37cu2rn755upcp6el');
assert.ok(hd._getInternalAddressByIndex(0) !== hd._getInternalAddressByIndex(1));
});
it('can generate', async () => {
let hd = new HDSegwitBech32Wallet();
let hashmap = {};
for (let c = 0; c < 1000; c++) {
await hd.generate();
let secret = hd.getSecret();
if (hashmap[secret]) {
throw new Error('Duplicate secret generated!');
}
hashmap[secret] = 1;
assert.ok(secret.split(' ').length === 12 || secret.split(' ').length === 24);
}
let hd2 = new HDSegwitBech32Wallet();
hd2.setSecret(hd.getSecret());
assert.ok(hd2.validateMnemonic());
});
});

View file

@ -9,7 +9,8 @@ jasmine.DEFAULT_TIMEOUT_INTERVAL = 300 * 1000;
afterAll(() => {
// after all tests we close socket so the test suite can actually terminate
return BlueElectrum.forceDisconnect();
BlueElectrum.forceDisconnect();
return new Promise(resolve => setTimeout(resolve, 10000)); // simple sleep to wait for all timeouts termination
});
beforeAll(async () => {
@ -79,7 +80,6 @@ it('HD (BIP49) can work with a gap', async function() {
// console.log('external', c, hd._getExternalAddressByIndex(c));
// }
await hd.fetchTransactions();
console.log('hd.transactions.length=', hd.transactions.length);
assert.ok(hd.transactions.length >= 3);
});
@ -90,7 +90,6 @@ it('Segwit HD (BIP49) can batch fetch many txs', async function() {
await hd.fetchBalance();
await hd.fetchTransactions();
assert.ok(hd.transactions.length > 0);
console.log('hd.transactions.length=', hd.transactions.length);
});
it('Segwit HD (BIP49) can generate addressess only via ypub', function() {

View file

@ -102,7 +102,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "4.0.0"
versionName "4.0.1"
ndk {
abiFilters "armeabi-v7a", "x86"
}

View file

@ -416,28 +416,30 @@ export class AbstractHDWallet extends LegacyWallet {
this.next_free_change_address_index = await binarySearchIterationForInternalAddress(100);
this.next_free_address_index = await binarySearchIterationForExternalAddress(100);
}
}
this.usedAddresses = [];
// generating all involved addresses:
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
this.usedAddresses.push(this._getExternalAddressByIndex(c));
}
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
this.usedAddresses.push(this._getInternalAddressByIndex(c));
}
} // end rescanning fresh wallet
// finally fetching balance
let balance = await BlueElectrum.multiGetBalanceByAddress(this.usedAddresses);
this.balance = balance.balance;
this.unconfirmed_balance = balance.unconfirmed_balance;
this._lastBalanceFetch = +new Date();
await this._fetchBalance();
} catch (err) {
console.warn(err);
}
}
async _fetchBalance() {
this.usedAddresses = [];
// generating all involved addresses:
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
this.usedAddresses.push(this._getExternalAddressByIndex(c));
}
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
this.usedAddresses.push(this._getInternalAddressByIndex(c));
}
let balance = await BlueElectrum.multiGetBalanceByAddress(this.usedAddresses);
this.balance = balance.balance;
this.unconfirmed_balance = balance.unconfirmed_balance;
this._lastBalanceFetch = +new Date();
}
async _fetchUtxoBatch(addresses) {
const api = new Frisbee({
baseURI: 'https://blockchain.info',

View file

@ -42,6 +42,10 @@ export class AbstractWallet {
return this.label;
}
/**
*
* @returns {number} Available to spend amount, int, in sats
*/
getBalance() {
return this.balance;
}

View file

@ -0,0 +1,333 @@
import { AbstractHDWallet } from './abstract-hd-wallet';
import { NativeModules } from 'react-native';
import bitcoin from 'bitcoinjs-lib';
import bip39 from 'bip39';
import BigNumber from 'bignumber.js';
import b58 from 'bs58check';
import signer from '../models/signer';
const BlueElectrum = require('../BlueElectrum');
const { RNRandomBytes } = NativeModules;
/**
* Converts zpub to xpub
*
* @param {String} zpub
* @returns {String} xpub
*/
function _zpubToXpub(zpub) {
let data = b58.decode(zpub);
data = data.slice(4);
data = Buffer.concat([Buffer.from('0488b21e', 'hex'), data]);
return b58.encode(data);
}
/**
* Creates Segwit Bech32 Bitcoin address
*
* @param hdNode
* @returns {String}
*/
function _nodeToBech32SegwitAddress(hdNode) {
const pubkeyBuf = hdNode.keyPair.getPublicKeyBuffer();
var scriptPubKey = bitcoin.script.witnessPubKeyHash.output.encode(bitcoin.crypto.hash160(pubkeyBuf));
var address = bitcoin.address.fromOutputScript(scriptPubKey);
return address;
}
/**
* HD Wallet (BIP39).
* In particular, BIP84 (Bech32 Native Segwit)
* @see https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki
*/
export class HDSegwitBech32Wallet extends AbstractHDWallet {
static type = 'HDsegwitBech32';
static typeReadable = 'HD SegWit (BIP84 Bech32 Native)';
constructor() {
super();
this._balances_by_external_index = {}; // 0 => { c: 0, u: 0 } // confirmed/unconfirmed
this._balances_by_internal_index = {};
this._txs_by_external_index = {};
this._txs_by_internal_index = {};
}
/**
* @inheritDoc
*/
getBalance() {
let ret = 0;
for (let bal of Object.values(this._balances_by_external_index)) {
ret += bal.c;
}
for (let bal of Object.values(this._balances_by_internal_index)) {
ret += bal.c;
}
return ret;
}
getUnconfirmedBalance() {
let ret = 0;
for (let bal of Object.values(this._balances_by_external_index)) {
ret += bal.u;
}
for (let bal of Object.values(this._balances_by_internal_index)) {
ret += bal.u;
}
return ret;
}
allowSend() {
return true;
}
async generate() {
let that = this;
return new Promise(function(resolve) {
if (typeof RNRandomBytes === 'undefined') {
// CLI/CI environment
// crypto should be provided globally by test launcher
return crypto.randomBytes(32, (err, buf) => { // eslint-disable-line
if (err) throw err;
that.secret = bip39.entropyToMnemonic(buf.toString('hex'));
resolve();
});
}
// RN environment
RNRandomBytes.randomBytes(32, (err, bytes) => {
if (err) throw new Error(err);
let b = Buffer.from(bytes, 'base64').toString('hex');
that.secret = bip39.entropyToMnemonic(b);
resolve();
});
});
}
_getExternalWIFByIndex(index) {
return this._getWIFByIndex(false, index);
}
_getInternalWIFByIndex(index) {
return this._getWIFByIndex(true, index);
}
/**
* Get internal/external WIF by wallet index
* @param {Boolean} internal
* @param {Number} index
* @returns {*}
* @private
*/
_getWIFByIndex(internal, index) {
const mnemonic = this.secret;
const seed = bip39.mnemonicToSeed(mnemonic);
const root = bitcoin.HDNode.fromSeedBuffer(seed);
const path = `m/84'/0'/0'/${internal ? 1 : 0}/${index}`;
const child = root.derivePath(path);
return child.keyPair.toWIF();
}
_getNodeAddressByIndex(node, index) {
index = index * 1; // cast to int
if (node === 0) {
if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit
}
if (node === 1) {
if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit
}
const xpub = _zpubToXpub(this.getXpub());
const hdNode = bitcoin.HDNode.fromBase58(xpub);
const address = _nodeToBech32SegwitAddress(hdNode.derive(node).derive(index));
if (node === 0) {
return (this.external_addresses_cache[index] = address);
}
if (node === 1) {
return (this.internal_addresses_cache[index] = address);
}
}
_getExternalAddressByIndex(index) {
return this._getNodeAddressByIndex(0, index);
}
_getInternalAddressByIndex(index) {
return this._getNodeAddressByIndex(1, index);
}
/**
* Returning zpub actually, not xpub. Keeping same method name
* for compatibility.
*
* @return {String} zpub
*/
getXpub() {
if (this._xpub) {
return this._xpub; // cache hit
}
// first, getting xpub
const mnemonic = this.secret;
const seed = bip39.mnemonicToSeed(mnemonic);
const root = bitcoin.HDNode.fromSeedBuffer(seed);
const path = "m/84'/0'/0'";
const child = root.derivePath(path).neutered();
const xpub = child.toBase58();
// bitcoinjs does not support zpub yet, so we just convert it from xpub
let data = b58.decode(xpub);
data = data.slice(4);
data = Buffer.concat([Buffer.from('04b24746', 'hex'), data]);
this._xpub = b58.encode(data);
return this._xpub;
}
/**
* @inheritDoc
*/
async fetchTransactions() {
// if txs are absent for some internal address in hierarchy - this is a sign
// we should fetch txs for that address
// OR if some address has unconfirmed balance - should fetch it's txs
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
if (!this._txs_by_external_index[c] || this._txs_by_external_index[c].length === 0 || this._balances_by_external_index[c].u !== 0) {
this._txs_by_external_index[c] = await BlueElectrum.getTransactionsFullByAddress(this._getExternalAddressByIndex(c));
}
}
for (let c = 0; c < this.next_free_change_address_index + 1 /* this.gap_limit */; c++) {
if (!this._txs_by_internal_index[c] || this._txs_by_internal_index[c].length === 0 || this._balances_by_internal_index[c].u !== 0) {
this._txs_by_internal_index[c] = await BlueElectrum.getTransactionsFullByAddress(this._getInternalAddressByIndex(c));
}
}
}
getTransactions() {
let txs = [];
for (let addressTxs of Object.values(this._txs_by_external_index)) {
txs = txs.concat(addressTxs);
}
for (let addressTxs of Object.values(this._txs_by_internal_index)) {
txs = txs.concat(addressTxs);
}
let ret = [];
for (let tx of txs) {
tx.timestamp = tx.blocktime;
tx.hash = tx.txid;
tx.value = 0;
for (let vin of tx.vin) {
// if input (spending) goes from our address - we are loosing!
if (vin.address && this.weOwnAddress(vin.address)) {
tx.value -= new BigNumber(vin.value).multipliedBy(100000000).toNumber();
}
}
for (let vout of tx.vout) {
// when output goes to our address - this means we are gaining!
if (
vout.scriptPubKey &&
vout.scriptPubKey.addresses &&
vout.scriptPubKey.addresses[0] &&
this.weOwnAddress(vout.scriptPubKey.addresses[0])
) {
tx.value += new BigNumber(vout.value).multipliedBy(100000000).toNumber();
}
}
ret.push(tx);
}
return ret;
}
async _fetchBalance() {
let addresses2fetch = [];
// generating all involved addresses.
// if address is skipped in internal representation (`_balances_by_external_index` and `_balances_by_internal_index`)
// then its a marker that this address should be fetched.
// if it has unconfirmed balance - it is also a marker that it should be fetched
// also it should be fetched if it is the last used address in hierarchy, just for any case,
// or if it is next unused (plus several unused addressess according to gap limit)
// external
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
if (c >= this.next_free_address_index) {
addresses2fetch.push(this._getExternalAddressByIndex(c));
} else if (!this._balances_by_external_index[c]) {
addresses2fetch.push(this._getExternalAddressByIndex(c));
} else if (this._balances_by_external_index[c] && this._balances_by_external_index[c].u !== 0) {
addresses2fetch.push(this._getExternalAddressByIndex(c));
}
}
// internal
for (let c = 0; c < this.next_free_change_address_index + 1 /* this.gap_limit */; c++) {
if (c >= this.next_free_change_address_index) {
addresses2fetch.push(this._getInternalAddressByIndex(c));
} else if (!this._balances_by_internal_index[c]) {
addresses2fetch.push(this._getInternalAddressByIndex(c));
} else if (this._balances_by_internal_index[c] && this._balances_by_internal_index[c].u !== 0) {
addresses2fetch.push(this._getInternalAddressByIndex(c));
}
}
let balances = await BlueElectrum.multiGetBalanceByAddress(addresses2fetch);
// converting to a more compact internal format
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
let addr = this._getExternalAddressByIndex(c);
if (balances.addresses[addr]) {
this._balances_by_external_index[c] = {
c: balances.addresses[addr].confirmed,
u: balances.addresses[addr].unconfirmed,
};
}
}
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
let addr = this._getInternalAddressByIndex(c);
if (balances.addresses[addr]) {
this._balances_by_internal_index[c] = {
c: balances.addresses[addr].confirmed,
u: balances.addresses[addr].unconfirmed,
};
}
}
this._lastBalanceFetch = +new Date();
}
weOwnAddress(address) {
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
if (this._getExternalAddressByIndex(c) === address) return true;
}
for (let c = 0; c < this.next_free_change_address_index + 1 /* this.gap_limit */; c++) {
if (this._getInternalAddressByIndex(c) === address) return true;
}
return false;
}
createTx(utxos, amount, fee, address) {
for (let utxo of utxos) {
utxo.wif = this._getWifForAddress(utxo.address);
}
let amountPlusFee = parseFloat(new BigNumber(amount).plus(fee).toString(10));
return signer.createHDSegwitTransaction(
utxos,
address,
amountPlusFee,
fee,
this._getInternalAddressByIndex(this.next_free_change_address_index),
);
}
}

View file

@ -10,3 +10,4 @@ export * from './hd-legacy-p2pkh-wallet';
export * from './watch-only-wallet';
export * from './lightning-custodian-wallet';
export * from './abstract-hd-wallet';
export * from './hd-segwit-bech32-wallet';

View file

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>4.0.0</string>
<string>4.0.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>

View file

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>4.0.0</string>
<string>4.0.1</string>
<key>CFBundleVersion</key>
<string>239</string>
<key>LSApplicationCategoryType</key>

View file

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

2
package-lock.json generated
View file

@ -1,6 +1,6 @@
{
"name": "BlueWallet",
"version": "4.0.0",
"version": "4.0.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View file

@ -1,6 +1,6 @@
{
"name": "BlueWallet",
"version": "4.0.0",
"version": "4.0.1",
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-eslint": "^10.0.1",