mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-03-03 20:07:11 +01:00
REF: BIP49 to use electrum
This commit is contained in:
parent
c975347b6f
commit
51e0d7d36c
4 changed files with 941 additions and 1072 deletions
891
class/abstract-hd-electrum-wallet.js
Normal file
891
class/abstract-hd-electrum-wallet.js
Normal file
|
@ -0,0 +1,891 @@
|
||||||
|
import { NativeModules } from 'react-native';
|
||||||
|
import bip39 from 'bip39';
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
import b58 from 'bs58check';
|
||||||
|
import { AbstractHDWallet } from './abstract-hd-wallet';
|
||||||
|
const bitcoin = require('bitcoinjs-lib');
|
||||||
|
const BlueElectrum = require('../BlueElectrum');
|
||||||
|
const HDNode = require('bip32');
|
||||||
|
const coinSelectAccumulative = require('coinselect/accumulative');
|
||||||
|
const coinSelectSplit = require('coinselect/split');
|
||||||
|
|
||||||
|
const { RNRandomBytes } = NativeModules;
|
||||||
|
|
||||||
|
export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
||||||
|
static type = 'abstract';
|
||||||
|
static typeReadable = 'abstract';
|
||||||
|
static defaultRBFSequence = 2147483648; // 1 << 31, minimum for replaceable transactions as per BIP68
|
||||||
|
static finalRBFSequence = 4294967295; // 0xFFFFFFFF
|
||||||
|
|
||||||
|
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 = {};
|
||||||
|
|
||||||
|
this._utxo = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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 + (this.getUnconfirmedBalance() < 0 ? this.getUnconfirmedBalance() : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
timeToRefreshTransaction() {
|
||||||
|
for (let tx of this.getTransactions()) {
|
||||||
|
if (tx.confirmations < 7) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {string|false} Either string WIF or FALSE if error happened
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_getWIFByIndex(internal, index) {
|
||||||
|
if (!this.secret) return false;
|
||||||
|
const mnemonic = this.secret;
|
||||||
|
const seed = bip39.mnemonicToSeed(mnemonic);
|
||||||
|
const root = HDNode.fromSeed(seed);
|
||||||
|
const path = `m/84'/0'/0'/${internal ? 1 : 0}/${index}`;
|
||||||
|
const child = root.derivePath(path);
|
||||||
|
|
||||||
|
return child.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
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node === 0 && !this._node0) {
|
||||||
|
const xpub = this.constructor._zpubToXpub(this.getXpub());
|
||||||
|
const hdNode = HDNode.fromBase58(xpub);
|
||||||
|
this._node0 = hdNode.derive(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node === 1 && !this._node1) {
|
||||||
|
const xpub = this.constructor._zpubToXpub(this.getXpub());
|
||||||
|
const hdNode = HDNode.fromBase58(xpub);
|
||||||
|
this._node1 = hdNode.derive(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
let address;
|
||||||
|
if (node === 0) {
|
||||||
|
address = this.constructor._nodeToBech32SegwitAddress(this._node0.derive(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node === 1) {
|
||||||
|
address = this.constructor._nodeToBech32SegwitAddress(this._node1.derive(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node === 0) {
|
||||||
|
return (this.external_addresses_cache[index] = address);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node === 1) {
|
||||||
|
return (this.internal_addresses_cache[index] = address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_getNodePubkeyByIndex(node, index) {
|
||||||
|
index = index * 1; // cast to int
|
||||||
|
|
||||||
|
if (node === 0 && !this._node0) {
|
||||||
|
const xpub = this.constructor._zpubToXpub(this.getXpub());
|
||||||
|
const hdNode = HDNode.fromBase58(xpub);
|
||||||
|
this._node0 = hdNode.derive(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node === 1 && !this._node1) {
|
||||||
|
const xpub = this.constructor._zpubToXpub(this.getXpub());
|
||||||
|
const hdNode = HDNode.fromBase58(xpub);
|
||||||
|
this._node1 = hdNode.derive(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node === 0) {
|
||||||
|
return this._node0.derive(index).publicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node === 1) {
|
||||||
|
return this._node1.derive(index).publicKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_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 = HDNode.fromSeed(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
|
||||||
|
// OR some tx for address is unconfirmed
|
||||||
|
// OR some tx has < 7 confirmations
|
||||||
|
|
||||||
|
// fetching transactions in batch: first, getting batch history for all addresses,
|
||||||
|
// then batch fetching all involved txids
|
||||||
|
// finally, batch fetching txids of all inputs (needed to see amounts & addresses of those inputs)
|
||||||
|
// then we combine it all together
|
||||||
|
|
||||||
|
let addresses2fetch = [];
|
||||||
|
|
||||||
|
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
|
||||||
|
// external addresses first
|
||||||
|
let hasUnconfirmed = false;
|
||||||
|
this._txs_by_external_index[c] = this._txs_by_external_index[c] || [];
|
||||||
|
for (let tx of this._txs_by_external_index[c]) hasUnconfirmed = hasUnconfirmed || !tx.confirmations || tx.confirmations < 7;
|
||||||
|
|
||||||
|
if (hasUnconfirmed || this._txs_by_external_index[c].length === 0 || this._balances_by_external_index[c].u !== 0) {
|
||||||
|
addresses2fetch.push(this._getExternalAddressByIndex(c));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
|
||||||
|
// next, internal addresses
|
||||||
|
let hasUnconfirmed = false;
|
||||||
|
this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || [];
|
||||||
|
for (let tx of this._txs_by_internal_index[c]) hasUnconfirmed = hasUnconfirmed || !tx.confirmations || tx.confirmations < 7;
|
||||||
|
|
||||||
|
if (hasUnconfirmed || this._txs_by_internal_index[c].length === 0 || this._balances_by_internal_index[c].u !== 0) {
|
||||||
|
addresses2fetch.push(this._getInternalAddressByIndex(c));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// first: batch fetch for all addresses histories
|
||||||
|
let histories = await BlueElectrum.multiGetHistoryByAddress(addresses2fetch);
|
||||||
|
let txs = {};
|
||||||
|
for (let history of Object.values(histories)) {
|
||||||
|
for (let tx of history) {
|
||||||
|
txs[tx.tx_hash] = tx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// next, batch fetching each txid we got
|
||||||
|
let txdatas = await BlueElectrum.multiGetTransactionByTxid(Object.keys(txs));
|
||||||
|
|
||||||
|
// now, tricky part. we collect all transactions from inputs (vin), and batch fetch them too.
|
||||||
|
// then we combine all this data (we need inputs to see source addresses and amounts)
|
||||||
|
let vinTxids = [];
|
||||||
|
for (let txdata of Object.values(txdatas)) {
|
||||||
|
for (let vin of txdata.vin) {
|
||||||
|
vinTxids.push(vin.txid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let vintxdatas = await BlueElectrum.multiGetTransactionByTxid(vinTxids);
|
||||||
|
|
||||||
|
// fetched all transactions from our inputs. now we need to combine it.
|
||||||
|
// iterating all _our_ transactions:
|
||||||
|
for (let txid of Object.keys(txdatas)) {
|
||||||
|
// iterating all inputs our our single transaction:
|
||||||
|
for (let inpNum = 0; inpNum < txdatas[txid].vin.length; inpNum++) {
|
||||||
|
let inpTxid = txdatas[txid].vin[inpNum].txid;
|
||||||
|
let inpVout = txdatas[txid].vin[inpNum].vout;
|
||||||
|
// got txid and output number of _previous_ transaction we shoud look into
|
||||||
|
if (vintxdatas[inpTxid] && vintxdatas[inpTxid].vout[inpVout]) {
|
||||||
|
// extracting amount & addresses from previous output and adding it to _our_ input:
|
||||||
|
txdatas[txid].vin[inpNum].addresses = vintxdatas[inpTxid].vout[inpVout].scriptPubKey.addresses;
|
||||||
|
txdatas[txid].vin[inpNum].value = vintxdatas[inpTxid].vout[inpVout].value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// now purge all unconfirmed txs from internal hashmaps, since some may be evicted from mempool because they became invalid
|
||||||
|
// or replaced. hashmaps are going to be re-populated anyways, since we fetched TXs for addresses with unconfirmed TXs
|
||||||
|
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
|
||||||
|
this._txs_by_external_index[c] = this._txs_by_external_index[c].filter(tx => !!tx.confirmations);
|
||||||
|
}
|
||||||
|
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
|
||||||
|
this._txs_by_internal_index[c] = this._txs_by_internal_index[c].filter(tx => !!tx.confirmations);
|
||||||
|
}
|
||||||
|
|
||||||
|
// now, we need to put transactions in all relevant `cells` of internal hashmaps: this._txs_by_internal_index && this._txs_by_external_index
|
||||||
|
|
||||||
|
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
|
||||||
|
for (let tx of Object.values(txdatas)) {
|
||||||
|
for (let vin of tx.vin) {
|
||||||
|
if (vin.addresses && vin.addresses.indexOf(this._getExternalAddressByIndex(c)) !== -1) {
|
||||||
|
// this TX is related to our address
|
||||||
|
this._txs_by_external_index[c] = this._txs_by_external_index[c] || [];
|
||||||
|
let clonedTx = Object.assign({}, tx);
|
||||||
|
clonedTx.inputs = tx.vin.slice(0);
|
||||||
|
clonedTx.outputs = tx.vout.slice(0);
|
||||||
|
delete clonedTx.vin;
|
||||||
|
delete clonedTx.vout;
|
||||||
|
|
||||||
|
// trying to replace tx if it exists already (because it has lower confirmations, for example)
|
||||||
|
let replaced = false;
|
||||||
|
for (let cc = 0; cc < this._txs_by_external_index[c].length; cc++) {
|
||||||
|
if (this._txs_by_external_index[c][cc].txid === clonedTx.txid) {
|
||||||
|
replaced = true;
|
||||||
|
this._txs_by_external_index[c][cc] = clonedTx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!replaced) this._txs_by_external_index[c].push(clonedTx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let vout of tx.vout) {
|
||||||
|
if (vout.scriptPubKey.addresses.indexOf(this._getExternalAddressByIndex(c)) !== -1) {
|
||||||
|
// this TX is related to our address
|
||||||
|
this._txs_by_external_index[c] = this._txs_by_external_index[c] || [];
|
||||||
|
let clonedTx = Object.assign({}, tx);
|
||||||
|
clonedTx.inputs = tx.vin.slice(0);
|
||||||
|
clonedTx.outputs = tx.vout.slice(0);
|
||||||
|
delete clonedTx.vin;
|
||||||
|
delete clonedTx.vout;
|
||||||
|
|
||||||
|
// trying to replace tx if it exists already (because it has lower confirmations, for example)
|
||||||
|
let replaced = false;
|
||||||
|
for (let cc = 0; cc < this._txs_by_external_index[c].length; cc++) {
|
||||||
|
if (this._txs_by_external_index[c][cc].txid === clonedTx.txid) {
|
||||||
|
replaced = true;
|
||||||
|
this._txs_by_external_index[c][cc] = clonedTx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!replaced) this._txs_by_external_index[c].push(clonedTx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
|
||||||
|
for (let tx of Object.values(txdatas)) {
|
||||||
|
for (let vin of tx.vin) {
|
||||||
|
if (vin.addresses && vin.addresses.indexOf(this._getInternalAddressByIndex(c)) !== -1) {
|
||||||
|
// this TX is related to our address
|
||||||
|
this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || [];
|
||||||
|
let clonedTx = Object.assign({}, tx);
|
||||||
|
clonedTx.inputs = tx.vin.slice(0);
|
||||||
|
clonedTx.outputs = tx.vout.slice(0);
|
||||||
|
delete clonedTx.vin;
|
||||||
|
delete clonedTx.vout;
|
||||||
|
|
||||||
|
// trying to replace tx if it exists already (because it has lower confirmations, for example)
|
||||||
|
let replaced = false;
|
||||||
|
for (let cc = 0; cc < this._txs_by_internal_index[c].length; cc++) {
|
||||||
|
if (this._txs_by_internal_index[c][cc].txid === clonedTx.txid) {
|
||||||
|
replaced = true;
|
||||||
|
this._txs_by_internal_index[c][cc] = clonedTx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!replaced) this._txs_by_internal_index[c].push(clonedTx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let vout of tx.vout) {
|
||||||
|
if (vout.scriptPubKey.addresses.indexOf(this._getInternalAddressByIndex(c)) !== -1) {
|
||||||
|
// this TX is related to our address
|
||||||
|
this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || [];
|
||||||
|
let clonedTx = Object.assign({}, tx);
|
||||||
|
clonedTx.inputs = tx.vin.slice(0);
|
||||||
|
clonedTx.outputs = tx.vout.slice(0);
|
||||||
|
delete clonedTx.vin;
|
||||||
|
delete clonedTx.vout;
|
||||||
|
|
||||||
|
// trying to replace tx if it exists already (because it has lower confirmations, for example)
|
||||||
|
let replaced = false;
|
||||||
|
for (let cc = 0; cc < this._txs_by_internal_index[c].length; cc++) {
|
||||||
|
if (this._txs_by_internal_index[c][cc].txid === clonedTx.txid) {
|
||||||
|
replaced = true;
|
||||||
|
this._txs_by_internal_index[c][cc] = clonedTx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!replaced) this._txs_by_internal_index[c].push(clonedTx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._lastTxFetch = +new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
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.received = tx.blocktime * 1000;
|
||||||
|
if (!tx.blocktime) tx.received = +new Date() - 30 * 1000; // unconfirmed
|
||||||
|
tx.confirmations = tx.confirmations || 0; // unconfirmed
|
||||||
|
tx.hash = tx.txid;
|
||||||
|
tx.value = 0;
|
||||||
|
|
||||||
|
for (let vin of tx.inputs) {
|
||||||
|
// if input (spending) goes from our address - we are loosing!
|
||||||
|
if ((vin.address && this.weOwnAddress(vin.address)) || (vin.addresses && vin.addresses[0] && this.weOwnAddress(vin.addresses[0]))) {
|
||||||
|
tx.value -= new BigNumber(vin.value).multipliedBy(100000000).toNumber();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let vout of tx.outputs) {
|
||||||
|
// when output goes to our address - this means we are gaining!
|
||||||
|
if (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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// now, deduplication:
|
||||||
|
let usedTxIds = {};
|
||||||
|
let ret2 = [];
|
||||||
|
for (let tx of ret) {
|
||||||
|
if (!usedTxIds[tx.txid]) ret2.push(tx);
|
||||||
|
usedTxIds[tx.txid] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret2.sort(function(a, b) {
|
||||||
|
return b.received - a.received;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async _binarySearchIterationForInternalAddress(index) {
|
||||||
|
const gerenateChunkAddresses = chunkNum => {
|
||||||
|
let ret = [];
|
||||||
|
for (let c = this.gap_limit * chunkNum; c < this.gap_limit * (chunkNum + 1); c++) {
|
||||||
|
ret.push(this._getInternalAddressByIndex(c));
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
|
||||||
|
let lastChunkWithUsedAddressesNum = null;
|
||||||
|
let lastHistoriesWithUsedAddresses = null;
|
||||||
|
for (let c = 0; c < Math.round(index / this.gap_limit); c++) {
|
||||||
|
let histories = await BlueElectrum.multiGetHistoryByAddress(gerenateChunkAddresses(c));
|
||||||
|
if (this.constructor._getTransactionsFromHistories(histories).length > 0) {
|
||||||
|
// in this particular chunk we have used addresses
|
||||||
|
lastChunkWithUsedAddressesNum = c;
|
||||||
|
lastHistoriesWithUsedAddresses = histories;
|
||||||
|
} else {
|
||||||
|
// empty chunk. no sense searching more chunks
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastUsedIndex = 0;
|
||||||
|
|
||||||
|
if (lastHistoriesWithUsedAddresses) {
|
||||||
|
// now searching for last used address in batch lastChunkWithUsedAddressesNum
|
||||||
|
for (
|
||||||
|
let c = lastChunkWithUsedAddressesNum * this.gap_limit;
|
||||||
|
c < lastChunkWithUsedAddressesNum * this.gap_limit + this.gap_limit;
|
||||||
|
c++
|
||||||
|
) {
|
||||||
|
let address = this._getInternalAddressByIndex(c);
|
||||||
|
if (lastHistoriesWithUsedAddresses[address] && lastHistoriesWithUsedAddresses[address].length > 0) {
|
||||||
|
lastUsedIndex = Math.max(c, lastUsedIndex) + 1; // point to next, which is supposed to be unsued
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastUsedIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _binarySearchIterationForExternalAddress(index) {
|
||||||
|
const gerenateChunkAddresses = chunkNum => {
|
||||||
|
let ret = [];
|
||||||
|
for (let c = this.gap_limit * chunkNum; c < this.gap_limit * (chunkNum + 1); c++) {
|
||||||
|
ret.push(this._getExternalAddressByIndex(c));
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
|
||||||
|
let lastChunkWithUsedAddressesNum = null;
|
||||||
|
let lastHistoriesWithUsedAddresses = null;
|
||||||
|
for (let c = 0; c < Math.round(index / this.gap_limit); c++) {
|
||||||
|
let histories = await BlueElectrum.multiGetHistoryByAddress(gerenateChunkAddresses(c));
|
||||||
|
if (this.constructor._getTransactionsFromHistories(histories).length > 0) {
|
||||||
|
// in this particular chunk we have used addresses
|
||||||
|
lastChunkWithUsedAddressesNum = c;
|
||||||
|
lastHistoriesWithUsedAddresses = histories;
|
||||||
|
} else {
|
||||||
|
// empty chunk. no sense searching more chunks
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastUsedIndex = 0;
|
||||||
|
|
||||||
|
if (lastHistoriesWithUsedAddresses) {
|
||||||
|
// now searching for last used address in batch lastChunkWithUsedAddressesNum
|
||||||
|
for (
|
||||||
|
let c = lastChunkWithUsedAddressesNum * this.gap_limit;
|
||||||
|
c < lastChunkWithUsedAddressesNum * this.gap_limit + this.gap_limit;
|
||||||
|
c++
|
||||||
|
) {
|
||||||
|
let address = this._getExternalAddressByIndex(c);
|
||||||
|
if (lastHistoriesWithUsedAddresses[address] && lastHistoriesWithUsedAddresses[address].length > 0) {
|
||||||
|
lastUsedIndex = Math.max(c, lastUsedIndex) + 1; // point to next, which is supposed to be unsued
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastUsedIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchBalance() {
|
||||||
|
try {
|
||||||
|
if (this.next_free_change_address_index === 0 && this.next_free_address_index === 0) {
|
||||||
|
// doing binary search for last used address:
|
||||||
|
this.next_free_change_address_index = await this._binarySearchIterationForInternalAddress(1000);
|
||||||
|
this.next_free_address_index = await this._binarySearchIterationForExternalAddress(1000);
|
||||||
|
} // end rescanning fresh wallet
|
||||||
|
|
||||||
|
// finally fetching balance
|
||||||
|
await this._fetchBalance();
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _fetchBalance() {
|
||||||
|
// probing future addressess in hierarchy whether they have any transactions, in case
|
||||||
|
// our 'next free addr' pointers are lagging behind
|
||||||
|
let tryAgain = false;
|
||||||
|
let txs = await BlueElectrum.getTransactionsByAddress(
|
||||||
|
this._getExternalAddressByIndex(this.next_free_address_index + this.gap_limit - 1),
|
||||||
|
);
|
||||||
|
if (txs.length > 0) {
|
||||||
|
// whoa, someone uses our wallet outside! better catch up
|
||||||
|
this.next_free_address_index += this.gap_limit;
|
||||||
|
tryAgain = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
txs = await BlueElectrum.getTransactionsByAddress(
|
||||||
|
this._getInternalAddressByIndex(this.next_free_change_address_index + this.gap_limit - 1),
|
||||||
|
);
|
||||||
|
if (txs.length > 0) {
|
||||||
|
this.next_free_change_address_index += this.gap_limit;
|
||||||
|
tryAgain = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: refactor me ^^^ can be batched in single call. plus not just couple of addresses, but all between [ next_free .. (next_free + gap_limit) ]
|
||||||
|
|
||||||
|
if (tryAgain) return this._fetchBalance();
|
||||||
|
|
||||||
|
// next, business as usuall. fetch balances
|
||||||
|
|
||||||
|
let addresses2fetch = [];
|
||||||
|
|
||||||
|
// generating all involved addresses.
|
||||||
|
// basically, refetch all from index zero to maximum. doesnt matter
|
||||||
|
// since we batch them 100 per call
|
||||||
|
|
||||||
|
// external
|
||||||
|
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
|
||||||
|
addresses2fetch.push(this._getExternalAddressByIndex(c));
|
||||||
|
}
|
||||||
|
|
||||||
|
// internal
|
||||||
|
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
|
||||||
|
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]) {
|
||||||
|
// first, if balances differ from what we store - we delete transactions for that
|
||||||
|
// address so next fetchTransactions() will refetch everything
|
||||||
|
if (this._balances_by_external_index[c]) {
|
||||||
|
if (
|
||||||
|
this._balances_by_external_index[c].c !== balances.addresses[addr].confirmed ||
|
||||||
|
this._balances_by_external_index[c].u !== balances.addresses[addr].unconfirmed
|
||||||
|
) {
|
||||||
|
delete this._txs_by_external_index[c];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// update local representation of balances on that address:
|
||||||
|
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]) {
|
||||||
|
// first, if balances differ from what we store - we delete transactions for that
|
||||||
|
// address so next fetchTransactions() will refetch everything
|
||||||
|
if (this._balances_by_internal_index[c]) {
|
||||||
|
if (
|
||||||
|
this._balances_by_internal_index[c].c !== balances.addresses[addr].confirmed ||
|
||||||
|
this._balances_by_internal_index[c].u !== balances.addresses[addr].unconfirmed
|
||||||
|
) {
|
||||||
|
delete this._txs_by_internal_index[c];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// update local representation of balances on that address:
|
||||||
|
this._balances_by_internal_index[c] = {
|
||||||
|
c: balances.addresses[addr].confirmed,
|
||||||
|
u: balances.addresses[addr].unconfirmed,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._lastBalanceFetch = +new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchUtxo() {
|
||||||
|
// considering only confirmed balance
|
||||||
|
let addressess = [];
|
||||||
|
|
||||||
|
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
|
||||||
|
if (this._balances_by_external_index[c] && this._balances_by_external_index[c].c && this._balances_by_external_index[c].c > 0) {
|
||||||
|
addressess.push(this._getExternalAddressByIndex(c));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
|
||||||
|
if (this._balances_by_internal_index[c] && this._balances_by_internal_index[c].c && this._balances_by_internal_index[c].c > 0) {
|
||||||
|
addressess.push(this._getInternalAddressByIndex(c));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._utxo = [];
|
||||||
|
for (let arr of Object.values(await BlueElectrum.multiGetUtxoByAddress(addressess))) {
|
||||||
|
this._utxo = this._utxo.concat(arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// backward compatibility TODO: remove when we make sure `.utxo` is not used
|
||||||
|
this.utxo = this._utxo;
|
||||||
|
// this belongs in `.getUtxo()`
|
||||||
|
for (let u of this.utxo) {
|
||||||
|
u.txid = u.txId;
|
||||||
|
u.amount = u.value;
|
||||||
|
u.wif = this._getWifForAddress(u.address);
|
||||||
|
u.confirmations = u.height ? 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getUtxo() {
|
||||||
|
return this._utxo;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getDerivationPathByAddress(address) {
|
||||||
|
const path = "m/84'/0'/0'";
|
||||||
|
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
|
||||||
|
if (this._getExternalAddressByIndex(c) === address) return path + '/0/' + c;
|
||||||
|
}
|
||||||
|
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
|
||||||
|
if (this._getInternalAddressByIndex(c) === address) return path + '/1/' + c;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getPubkeyByAddress(address) {
|
||||||
|
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
|
||||||
|
if (this._getExternalAddressByIndex(c) === address) return this._getNodePubkeyByIndex(0, c);
|
||||||
|
}
|
||||||
|
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
|
||||||
|
if (this._getInternalAddressByIndex(c) === address) return this._getNodePubkeyByIndex(1, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 + this.gap_limit; c++) {
|
||||||
|
if (this._getInternalAddressByIndex(c) === address) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
createTx(utxos, amount, fee, address) {
|
||||||
|
throw new Error('Deprecated');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param utxos {Array.<{vout: Number, value: Number, txId: String, address: String}>} List of spendable utxos
|
||||||
|
* @param targets {Array.<{value: Number, address: String}>} Where coins are going. If theres only 1 target and that target has no value - this will send MAX to that address (respecting fee rate)
|
||||||
|
* @param feeRate {Number} satoshi per byte
|
||||||
|
* @param changeAddress {String} Excessive coins will go back to that address
|
||||||
|
* @param sequence {Number} Used in RBF
|
||||||
|
* @param skipSigning {boolean} Whether we should skip signing, use returned `psbt` in that case
|
||||||
|
* @returns {{outputs: Array, tx: Transaction, inputs: Array, fee: Number, psbt: Psbt}}
|
||||||
|
*/
|
||||||
|
createTransaction(utxos, targets, feeRate, changeAddress, sequence, skipSigning = false) {
|
||||||
|
if (!changeAddress) throw new Error('No change address provided');
|
||||||
|
sequence = sequence || AbstractHDElectrumWallet.defaultRBFSequence;
|
||||||
|
|
||||||
|
let algo = coinSelectAccumulative;
|
||||||
|
if (targets.length === 1 && targets[0] && !targets[0].value) {
|
||||||
|
// we want to send MAX
|
||||||
|
algo = coinSelectSplit;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { inputs, outputs, fee } = algo(utxos, targets, feeRate);
|
||||||
|
|
||||||
|
// .inputs and .outputs will be undefined if no solution was found
|
||||||
|
if (!inputs || !outputs) {
|
||||||
|
throw new Error('Not enough balance. Try sending smaller amount');
|
||||||
|
}
|
||||||
|
|
||||||
|
let psbt = new bitcoin.Psbt();
|
||||||
|
|
||||||
|
let c = 0;
|
||||||
|
let keypairs = {};
|
||||||
|
let values = {};
|
||||||
|
|
||||||
|
inputs.forEach(input => {
|
||||||
|
let keyPair;
|
||||||
|
if (!skipSigning) {
|
||||||
|
// skiping signing related stuff
|
||||||
|
keyPair = bitcoin.ECPair.fromWIF(this._getWifForAddress(input.address));
|
||||||
|
keypairs[c] = keyPair;
|
||||||
|
}
|
||||||
|
values[c] = input.value;
|
||||||
|
c++;
|
||||||
|
if (!skipSigning) {
|
||||||
|
// skiping signing related stuff
|
||||||
|
if (!input.address || !this._getWifForAddress(input.address)) throw new Error('Internal error: no address or WIF to sign input');
|
||||||
|
}
|
||||||
|
let pubkey = this._getPubkeyByAddress(input.address);
|
||||||
|
let masterFingerprint = Buffer.from([0x00, 0x00, 0x00, 0x00]);
|
||||||
|
// this is not correct fingerprint, as we dont know real fingerprint - we got zpub with 84/0, but fingerpting
|
||||||
|
// should be from root. basically, fingerprint should be provided from outside by user when importing zpub
|
||||||
|
let path = this._getDerivationPathByAddress(input.address);
|
||||||
|
const p2wpkh = bitcoin.payments.p2wpkh({ pubkey });
|
||||||
|
psbt.addInput({
|
||||||
|
hash: input.txId,
|
||||||
|
index: input.vout,
|
||||||
|
sequence,
|
||||||
|
bip32Derivation: [
|
||||||
|
{
|
||||||
|
masterFingerprint,
|
||||||
|
path,
|
||||||
|
pubkey,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
witnessUtxo: {
|
||||||
|
script: p2wpkh.output,
|
||||||
|
value: input.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
outputs.forEach(output => {
|
||||||
|
// if output has no address - this is change output
|
||||||
|
let change = false;
|
||||||
|
if (!output.address) {
|
||||||
|
change = true;
|
||||||
|
output.address = changeAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = this._getDerivationPathByAddress(output.address);
|
||||||
|
let pubkey = this._getPubkeyByAddress(output.address);
|
||||||
|
let masterFingerprint = Buffer.from([0x00, 0x00, 0x00, 0x00]);
|
||||||
|
// this is not correct fingerprint, as we dont know realfingerprint - we got zpub with 84/0, but fingerpting
|
||||||
|
// should be from root. basically, fingerprint should be provided from outside by user when importing zpub
|
||||||
|
|
||||||
|
let outputData = {
|
||||||
|
address: output.address,
|
||||||
|
value: output.value,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (change) {
|
||||||
|
outputData['bip32Derivation'] = [
|
||||||
|
{
|
||||||
|
masterFingerprint,
|
||||||
|
path,
|
||||||
|
pubkey,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
psbt.addOutput(outputData);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!skipSigning) {
|
||||||
|
// skiping signing related stuff
|
||||||
|
for (let cc = 0; cc < c; cc++) {
|
||||||
|
psbt.signInput(cc, keypairs[cc]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let tx;
|
||||||
|
if (!skipSigning) {
|
||||||
|
tx = psbt.finalizeAllInputs().extractTransaction();
|
||||||
|
}
|
||||||
|
return { tx, inputs, outputs, fee, psbt };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combines 2 PSBTs into final transaction from which you can
|
||||||
|
* get HEX and broadcast
|
||||||
|
*
|
||||||
|
* @param base64one {string}
|
||||||
|
* @param base64two {string}
|
||||||
|
* @returns {Transaction}
|
||||||
|
*/
|
||||||
|
combinePsbt(base64one, base64two) {
|
||||||
|
const final1 = bitcoin.Psbt.fromBase64(base64one);
|
||||||
|
const final2 = bitcoin.Psbt.fromBase64(base64two);
|
||||||
|
final1.combine(final2);
|
||||||
|
return final1.finalizeAllInputs().extractTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates Segwit Bech32 Bitcoin address
|
||||||
|
*
|
||||||
|
* @param hdNode
|
||||||
|
* @returns {String}
|
||||||
|
*/
|
||||||
|
static _nodeToBech32SegwitAddress(hdNode) {
|
||||||
|
return bitcoin.payments.p2wpkh({
|
||||||
|
pubkey: hdNode.publicKey,
|
||||||
|
}).address;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts zpub to xpub
|
||||||
|
*
|
||||||
|
* @param {String} zpub
|
||||||
|
* @returns {String} xpub
|
||||||
|
*/
|
||||||
|
static _zpubToXpub(zpub) {
|
||||||
|
let data = b58.decode(zpub);
|
||||||
|
data = data.slice(4);
|
||||||
|
data = Buffer.concat([Buffer.from('0488b21e', 'hex'), data]);
|
||||||
|
|
||||||
|
return b58.encode(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
static _getTransactionsFromHistories(histories) {
|
||||||
|
let txs = [];
|
||||||
|
for (let history of Object.values(histories)) {
|
||||||
|
for (let tx of history) {
|
||||||
|
txs.push(tx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return txs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast txhex. Can throw an exception if failed
|
||||||
|
*
|
||||||
|
* @param {String} txhex
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async broadcastTx(txhex) {
|
||||||
|
let broadcast = await BlueElectrum.broadcastV2(txhex);
|
||||||
|
console.log({ broadcast });
|
||||||
|
if (broadcast.indexOf('successfully') !== -1) return true;
|
||||||
|
return broadcast.length === 64; // this means return string is txid (precise length), so it was broadcasted ok
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,898 +1,23 @@
|
||||||
import { AbstractHDWallet } from './abstract-hd-wallet';
|
import { AbstractHDElectrumWallet } from './abstract-hd-electrum-wallet';
|
||||||
import { NativeModules } from 'react-native';
|
|
||||||
import bip39 from 'bip39';
|
|
||||||
import BigNumber from 'bignumber.js';
|
|
||||||
import b58 from 'bs58check';
|
|
||||||
const bitcoin = require('bitcoinjs-lib');
|
|
||||||
const BlueElectrum = require('../BlueElectrum');
|
|
||||||
const HDNode = require('bip32');
|
|
||||||
const coinSelectAccumulative = require('coinselect/accumulative');
|
|
||||||
const coinSelectSplit = require('coinselect/split');
|
|
||||||
|
|
||||||
const { RNRandomBytes } = NativeModules;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HD Wallet (BIP39).
|
* HD Wallet (BIP39).
|
||||||
* In particular, BIP84 (Bech32 Native Segwit)
|
* In particular, BIP84 (Bech32 Native Segwit)
|
||||||
* @see https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki
|
* @see https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki
|
||||||
*/
|
*/
|
||||||
export class HDSegwitBech32Wallet extends AbstractHDWallet {
|
export class HDSegwitBech32Wallet extends AbstractHDElectrumWallet {
|
||||||
static type = 'HDsegwitBech32';
|
static type = 'HDsegwitBech32';
|
||||||
static typeReadable = 'HD SegWit (BIP84 Bech32 Native)';
|
static typeReadable = 'HD SegWit (BIP84 Bech32 Native)';
|
||||||
static defaultRBFSequence = 2147483648; // 1 << 31, minimum for replaceable transactions as per BIP68
|
|
||||||
static finalRBFSequence = 4294967295; // 0xFFFFFFFF
|
|
||||||
|
|
||||||
constructor() {
|
allowSend() {
|
||||||
super();
|
return true;
|
||||||
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 = {};
|
|
||||||
|
|
||||||
this._utxo = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
allowBatchSend() {
|
allowBatchSend() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
allowSendMax(): boolean {
|
allowSendMax() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @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 + (this.getUnconfirmedBalance() < 0 ? this.getUnconfirmedBalance() : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritDoc
|
|
||||||
*/
|
|
||||||
timeToRefreshTransaction() {
|
|
||||||
for (let tx of this.getTransactions()) {
|
|
||||||
if (tx.confirmations < 7) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {string|false} Either string WIF or FALSE if error happened
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_getWIFByIndex(internal, index) {
|
|
||||||
if (!this.secret) return false;
|
|
||||||
const mnemonic = this.secret;
|
|
||||||
const seed = bip39.mnemonicToSeed(mnemonic);
|
|
||||||
const root = HDNode.fromSeed(seed);
|
|
||||||
const path = `m/84'/0'/0'/${internal ? 1 : 0}/${index}`;
|
|
||||||
const child = root.derivePath(path);
|
|
||||||
|
|
||||||
return child.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
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node === 0 && !this._node0) {
|
|
||||||
const xpub = this.constructor._zpubToXpub(this.getXpub());
|
|
||||||
const hdNode = HDNode.fromBase58(xpub);
|
|
||||||
this._node0 = hdNode.derive(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node === 1 && !this._node1) {
|
|
||||||
const xpub = this.constructor._zpubToXpub(this.getXpub());
|
|
||||||
const hdNode = HDNode.fromBase58(xpub);
|
|
||||||
this._node1 = hdNode.derive(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
let address;
|
|
||||||
if (node === 0) {
|
|
||||||
address = this.constructor._nodeToBech32SegwitAddress(this._node0.derive(index));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node === 1) {
|
|
||||||
address = this.constructor._nodeToBech32SegwitAddress(this._node1.derive(index));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node === 0) {
|
|
||||||
return (this.external_addresses_cache[index] = address);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node === 1) {
|
|
||||||
return (this.internal_addresses_cache[index] = address);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_getNodePubkeyByIndex(node, index) {
|
|
||||||
index = index * 1; // cast to int
|
|
||||||
|
|
||||||
if (node === 0 && !this._node0) {
|
|
||||||
const xpub = this.constructor._zpubToXpub(this.getXpub());
|
|
||||||
const hdNode = HDNode.fromBase58(xpub);
|
|
||||||
this._node0 = hdNode.derive(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node === 1 && !this._node1) {
|
|
||||||
const xpub = this.constructor._zpubToXpub(this.getXpub());
|
|
||||||
const hdNode = HDNode.fromBase58(xpub);
|
|
||||||
this._node1 = hdNode.derive(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node === 0) {
|
|
||||||
return this._node0.derive(index).publicKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node === 1) {
|
|
||||||
return this._node1.derive(index).publicKey;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_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 = HDNode.fromSeed(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
|
|
||||||
// OR some tx for address is unconfirmed
|
|
||||||
// OR some tx has < 7 confirmations
|
|
||||||
|
|
||||||
// fetching transactions in batch: first, getting batch history for all addresses,
|
|
||||||
// then batch fetching all involved txids
|
|
||||||
// finally, batch fetching txids of all inputs (needed to see amounts & addresses of those inputs)
|
|
||||||
// then we combine it all together
|
|
||||||
|
|
||||||
let addresses2fetch = [];
|
|
||||||
|
|
||||||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
|
|
||||||
// external addresses first
|
|
||||||
let hasUnconfirmed = false;
|
|
||||||
this._txs_by_external_index[c] = this._txs_by_external_index[c] || [];
|
|
||||||
for (let tx of this._txs_by_external_index[c]) hasUnconfirmed = hasUnconfirmed || !tx.confirmations || tx.confirmations < 7;
|
|
||||||
|
|
||||||
if (hasUnconfirmed || this._txs_by_external_index[c].length === 0 || this._balances_by_external_index[c].u !== 0) {
|
|
||||||
addresses2fetch.push(this._getExternalAddressByIndex(c));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
|
|
||||||
// next, internal addresses
|
|
||||||
let hasUnconfirmed = false;
|
|
||||||
this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || [];
|
|
||||||
for (let tx of this._txs_by_internal_index[c]) hasUnconfirmed = hasUnconfirmed || !tx.confirmations || tx.confirmations < 7;
|
|
||||||
|
|
||||||
if (hasUnconfirmed || this._txs_by_internal_index[c].length === 0 || this._balances_by_internal_index[c].u !== 0) {
|
|
||||||
addresses2fetch.push(this._getInternalAddressByIndex(c));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// first: batch fetch for all addresses histories
|
|
||||||
let histories = await BlueElectrum.multiGetHistoryByAddress(addresses2fetch);
|
|
||||||
let txs = {};
|
|
||||||
for (let history of Object.values(histories)) {
|
|
||||||
for (let tx of history) {
|
|
||||||
txs[tx.tx_hash] = tx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// next, batch fetching each txid we got
|
|
||||||
let txdatas = await BlueElectrum.multiGetTransactionByTxid(Object.keys(txs));
|
|
||||||
|
|
||||||
// now, tricky part. we collect all transactions from inputs (vin), and batch fetch them too.
|
|
||||||
// then we combine all this data (we need inputs to see source addresses and amounts)
|
|
||||||
let vinTxids = [];
|
|
||||||
for (let txdata of Object.values(txdatas)) {
|
|
||||||
for (let vin of txdata.vin) {
|
|
||||||
vinTxids.push(vin.txid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let vintxdatas = await BlueElectrum.multiGetTransactionByTxid(vinTxids);
|
|
||||||
|
|
||||||
// fetched all transactions from our inputs. now we need to combine it.
|
|
||||||
// iterating all _our_ transactions:
|
|
||||||
for (let txid of Object.keys(txdatas)) {
|
|
||||||
// iterating all inputs our our single transaction:
|
|
||||||
for (let inpNum = 0; inpNum < txdatas[txid].vin.length; inpNum++) {
|
|
||||||
let inpTxid = txdatas[txid].vin[inpNum].txid;
|
|
||||||
let inpVout = txdatas[txid].vin[inpNum].vout;
|
|
||||||
// got txid and output number of _previous_ transaction we shoud look into
|
|
||||||
if (vintxdatas[inpTxid] && vintxdatas[inpTxid].vout[inpVout]) {
|
|
||||||
// extracting amount & addresses from previous output and adding it to _our_ input:
|
|
||||||
txdatas[txid].vin[inpNum].addresses = vintxdatas[inpTxid].vout[inpVout].scriptPubKey.addresses;
|
|
||||||
txdatas[txid].vin[inpNum].value = vintxdatas[inpTxid].vout[inpVout].value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// now purge all unconfirmed txs from internal hashmaps, since some may be evicted from mempool because they became invalid
|
|
||||||
// or replaced. hashmaps are going to be re-populated anyways, since we fetched TXs for addresses with unconfirmed TXs
|
|
||||||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
|
|
||||||
this._txs_by_external_index[c] = this._txs_by_external_index[c].filter(tx => !!tx.confirmations);
|
|
||||||
}
|
|
||||||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
|
|
||||||
this._txs_by_internal_index[c] = this._txs_by_internal_index[c].filter(tx => !!tx.confirmations);
|
|
||||||
}
|
|
||||||
|
|
||||||
// now, we need to put transactions in all relevant `cells` of internal hashmaps: this._txs_by_internal_index && this._txs_by_external_index
|
|
||||||
|
|
||||||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
|
|
||||||
for (let tx of Object.values(txdatas)) {
|
|
||||||
for (let vin of tx.vin) {
|
|
||||||
if (vin.addresses && vin.addresses.indexOf(this._getExternalAddressByIndex(c)) !== -1) {
|
|
||||||
// this TX is related to our address
|
|
||||||
this._txs_by_external_index[c] = this._txs_by_external_index[c] || [];
|
|
||||||
let clonedTx = Object.assign({}, tx);
|
|
||||||
clonedTx.inputs = tx.vin.slice(0);
|
|
||||||
clonedTx.outputs = tx.vout.slice(0);
|
|
||||||
delete clonedTx.vin;
|
|
||||||
delete clonedTx.vout;
|
|
||||||
|
|
||||||
// trying to replace tx if it exists already (because it has lower confirmations, for example)
|
|
||||||
let replaced = false;
|
|
||||||
for (let cc = 0; cc < this._txs_by_external_index[c].length; cc++) {
|
|
||||||
if (this._txs_by_external_index[c][cc].txid === clonedTx.txid) {
|
|
||||||
replaced = true;
|
|
||||||
this._txs_by_external_index[c][cc] = clonedTx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!replaced) this._txs_by_external_index[c].push(clonedTx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (let vout of tx.vout) {
|
|
||||||
if (vout.scriptPubKey.addresses.indexOf(this._getExternalAddressByIndex(c)) !== -1) {
|
|
||||||
// this TX is related to our address
|
|
||||||
this._txs_by_external_index[c] = this._txs_by_external_index[c] || [];
|
|
||||||
let clonedTx = Object.assign({}, tx);
|
|
||||||
clonedTx.inputs = tx.vin.slice(0);
|
|
||||||
clonedTx.outputs = tx.vout.slice(0);
|
|
||||||
delete clonedTx.vin;
|
|
||||||
delete clonedTx.vout;
|
|
||||||
|
|
||||||
// trying to replace tx if it exists already (because it has lower confirmations, for example)
|
|
||||||
let replaced = false;
|
|
||||||
for (let cc = 0; cc < this._txs_by_external_index[c].length; cc++) {
|
|
||||||
if (this._txs_by_external_index[c][cc].txid === clonedTx.txid) {
|
|
||||||
replaced = true;
|
|
||||||
this._txs_by_external_index[c][cc] = clonedTx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!replaced) this._txs_by_external_index[c].push(clonedTx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
|
|
||||||
for (let tx of Object.values(txdatas)) {
|
|
||||||
for (let vin of tx.vin) {
|
|
||||||
if (vin.addresses && vin.addresses.indexOf(this._getInternalAddressByIndex(c)) !== -1) {
|
|
||||||
// this TX is related to our address
|
|
||||||
this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || [];
|
|
||||||
let clonedTx = Object.assign({}, tx);
|
|
||||||
clonedTx.inputs = tx.vin.slice(0);
|
|
||||||
clonedTx.outputs = tx.vout.slice(0);
|
|
||||||
delete clonedTx.vin;
|
|
||||||
delete clonedTx.vout;
|
|
||||||
|
|
||||||
// trying to replace tx if it exists already (because it has lower confirmations, for example)
|
|
||||||
let replaced = false;
|
|
||||||
for (let cc = 0; cc < this._txs_by_internal_index[c].length; cc++) {
|
|
||||||
if (this._txs_by_internal_index[c][cc].txid === clonedTx.txid) {
|
|
||||||
replaced = true;
|
|
||||||
this._txs_by_internal_index[c][cc] = clonedTx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!replaced) this._txs_by_internal_index[c].push(clonedTx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (let vout of tx.vout) {
|
|
||||||
if (vout.scriptPubKey.addresses.indexOf(this._getInternalAddressByIndex(c)) !== -1) {
|
|
||||||
// this TX is related to our address
|
|
||||||
this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || [];
|
|
||||||
let clonedTx = Object.assign({}, tx);
|
|
||||||
clonedTx.inputs = tx.vin.slice(0);
|
|
||||||
clonedTx.outputs = tx.vout.slice(0);
|
|
||||||
delete clonedTx.vin;
|
|
||||||
delete clonedTx.vout;
|
|
||||||
|
|
||||||
// trying to replace tx if it exists already (because it has lower confirmations, for example)
|
|
||||||
let replaced = false;
|
|
||||||
for (let cc = 0; cc < this._txs_by_internal_index[c].length; cc++) {
|
|
||||||
if (this._txs_by_internal_index[c][cc].txid === clonedTx.txid) {
|
|
||||||
replaced = true;
|
|
||||||
this._txs_by_internal_index[c][cc] = clonedTx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!replaced) this._txs_by_internal_index[c].push(clonedTx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this._lastTxFetch = +new Date();
|
|
||||||
}
|
|
||||||
|
|
||||||
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.received = tx.blocktime * 1000;
|
|
||||||
if (!tx.blocktime) tx.received = +new Date() - 30 * 1000; // unconfirmed
|
|
||||||
tx.confirmations = tx.confirmations || 0; // unconfirmed
|
|
||||||
tx.hash = tx.txid;
|
|
||||||
tx.value = 0;
|
|
||||||
|
|
||||||
for (let vin of tx.inputs) {
|
|
||||||
// if input (spending) goes from our address - we are loosing!
|
|
||||||
if ((vin.address && this.weOwnAddress(vin.address)) || (vin.addresses && vin.addresses[0] && this.weOwnAddress(vin.addresses[0]))) {
|
|
||||||
tx.value -= new BigNumber(vin.value).multipliedBy(100000000).toNumber();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let vout of tx.outputs) {
|
|
||||||
// when output goes to our address - this means we are gaining!
|
|
||||||
if (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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// now, deduplication:
|
|
||||||
let usedTxIds = {};
|
|
||||||
let ret2 = [];
|
|
||||||
for (let tx of ret) {
|
|
||||||
if (!usedTxIds[tx.txid]) ret2.push(tx);
|
|
||||||
usedTxIds[tx.txid] = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret2.sort(function(a, b) {
|
|
||||||
return b.received - a.received;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async _binarySearchIterationForInternalAddress(index) {
|
|
||||||
const gerenateChunkAddresses = chunkNum => {
|
|
||||||
let ret = [];
|
|
||||||
for (let c = this.gap_limit * chunkNum; c < this.gap_limit * (chunkNum + 1); c++) {
|
|
||||||
ret.push(this._getInternalAddressByIndex(c));
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
|
|
||||||
let lastChunkWithUsedAddressesNum = null;
|
|
||||||
let lastHistoriesWithUsedAddresses = null;
|
|
||||||
for (let c = 0; c < Math.round(index / this.gap_limit); c++) {
|
|
||||||
let histories = await BlueElectrum.multiGetHistoryByAddress(gerenateChunkAddresses(c));
|
|
||||||
if (this.constructor._getTransactionsFromHistories(histories).length > 0) {
|
|
||||||
// in this particular chunk we have used addresses
|
|
||||||
lastChunkWithUsedAddressesNum = c;
|
|
||||||
lastHistoriesWithUsedAddresses = histories;
|
|
||||||
} else {
|
|
||||||
// empty chunk. no sense searching more chunks
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let lastUsedIndex = 0;
|
|
||||||
|
|
||||||
if (lastHistoriesWithUsedAddresses) {
|
|
||||||
// now searching for last used address in batch lastChunkWithUsedAddressesNum
|
|
||||||
for (
|
|
||||||
let c = lastChunkWithUsedAddressesNum * this.gap_limit;
|
|
||||||
c < lastChunkWithUsedAddressesNum * this.gap_limit + this.gap_limit;
|
|
||||||
c++
|
|
||||||
) {
|
|
||||||
let address = this._getInternalAddressByIndex(c);
|
|
||||||
if (lastHistoriesWithUsedAddresses[address] && lastHistoriesWithUsedAddresses[address].length > 0) {
|
|
||||||
lastUsedIndex = Math.max(c, lastUsedIndex) + 1; // point to next, which is supposed to be unsued
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return lastUsedIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
async _binarySearchIterationForExternalAddress(index) {
|
|
||||||
const gerenateChunkAddresses = chunkNum => {
|
|
||||||
let ret = [];
|
|
||||||
for (let c = this.gap_limit * chunkNum; c < this.gap_limit * (chunkNum + 1); c++) {
|
|
||||||
ret.push(this._getExternalAddressByIndex(c));
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
|
|
||||||
let lastChunkWithUsedAddressesNum = null;
|
|
||||||
let lastHistoriesWithUsedAddresses = null;
|
|
||||||
for (let c = 0; c < Math.round(index / this.gap_limit); c++) {
|
|
||||||
let histories = await BlueElectrum.multiGetHistoryByAddress(gerenateChunkAddresses(c));
|
|
||||||
if (this.constructor._getTransactionsFromHistories(histories).length > 0) {
|
|
||||||
// in this particular chunk we have used addresses
|
|
||||||
lastChunkWithUsedAddressesNum = c;
|
|
||||||
lastHistoriesWithUsedAddresses = histories;
|
|
||||||
} else {
|
|
||||||
// empty chunk. no sense searching more chunks
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let lastUsedIndex = 0;
|
|
||||||
|
|
||||||
if (lastHistoriesWithUsedAddresses) {
|
|
||||||
// now searching for last used address in batch lastChunkWithUsedAddressesNum
|
|
||||||
for (
|
|
||||||
let c = lastChunkWithUsedAddressesNum * this.gap_limit;
|
|
||||||
c < lastChunkWithUsedAddressesNum * this.gap_limit + this.gap_limit;
|
|
||||||
c++
|
|
||||||
) {
|
|
||||||
let address = this._getExternalAddressByIndex(c);
|
|
||||||
if (lastHistoriesWithUsedAddresses[address] && lastHistoriesWithUsedAddresses[address].length > 0) {
|
|
||||||
lastUsedIndex = Math.max(c, lastUsedIndex) + 1; // point to next, which is supposed to be unsued
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return lastUsedIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchBalance() {
|
|
||||||
try {
|
|
||||||
if (this.next_free_change_address_index === 0 && this.next_free_address_index === 0) {
|
|
||||||
// doing binary search for last used address:
|
|
||||||
this.next_free_change_address_index = await this._binarySearchIterationForInternalAddress(1000);
|
|
||||||
this.next_free_address_index = await this._binarySearchIterationForExternalAddress(1000);
|
|
||||||
} // end rescanning fresh wallet
|
|
||||||
|
|
||||||
// finally fetching balance
|
|
||||||
await this._fetchBalance();
|
|
||||||
} catch (err) {
|
|
||||||
console.warn(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async _fetchBalance() {
|
|
||||||
// probing future addressess in hierarchy whether they have any transactions, in case
|
|
||||||
// our 'next free addr' pointers are lagging behind
|
|
||||||
let tryAgain = false;
|
|
||||||
let txs = await BlueElectrum.getTransactionsByAddress(
|
|
||||||
this._getExternalAddressByIndex(this.next_free_address_index + this.gap_limit - 1),
|
|
||||||
);
|
|
||||||
if (txs.length > 0) {
|
|
||||||
// whoa, someone uses our wallet outside! better catch up
|
|
||||||
this.next_free_address_index += this.gap_limit;
|
|
||||||
tryAgain = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
txs = await BlueElectrum.getTransactionsByAddress(
|
|
||||||
this._getInternalAddressByIndex(this.next_free_change_address_index + this.gap_limit - 1),
|
|
||||||
);
|
|
||||||
if (txs.length > 0) {
|
|
||||||
this.next_free_change_address_index += this.gap_limit;
|
|
||||||
tryAgain = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: refactor me ^^^ can be batched in single call. plus not just couple of addresses, but all between [ next_free .. (next_free + gap_limit) ]
|
|
||||||
|
|
||||||
if (tryAgain) return this._fetchBalance();
|
|
||||||
|
|
||||||
// next, business as usuall. fetch balances
|
|
||||||
|
|
||||||
let addresses2fetch = [];
|
|
||||||
|
|
||||||
// generating all involved addresses.
|
|
||||||
// basically, refetch all from index zero to maximum. doesnt matter
|
|
||||||
// since we batch them 100 per call
|
|
||||||
|
|
||||||
// external
|
|
||||||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
|
|
||||||
addresses2fetch.push(this._getExternalAddressByIndex(c));
|
|
||||||
}
|
|
||||||
|
|
||||||
// internal
|
|
||||||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
|
|
||||||
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]) {
|
|
||||||
// first, if balances differ from what we store - we delete transactions for that
|
|
||||||
// address so next fetchTransactions() will refetch everything
|
|
||||||
if (this._balances_by_external_index[c]) {
|
|
||||||
if (
|
|
||||||
this._balances_by_external_index[c].c !== balances.addresses[addr].confirmed ||
|
|
||||||
this._balances_by_external_index[c].u !== balances.addresses[addr].unconfirmed
|
|
||||||
) {
|
|
||||||
delete this._txs_by_external_index[c];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// update local representation of balances on that address:
|
|
||||||
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]) {
|
|
||||||
// first, if balances differ from what we store - we delete transactions for that
|
|
||||||
// address so next fetchTransactions() will refetch everything
|
|
||||||
if (this._balances_by_internal_index[c]) {
|
|
||||||
if (
|
|
||||||
this._balances_by_internal_index[c].c !== balances.addresses[addr].confirmed ||
|
|
||||||
this._balances_by_internal_index[c].u !== balances.addresses[addr].unconfirmed
|
|
||||||
) {
|
|
||||||
delete this._txs_by_internal_index[c];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// update local representation of balances on that address:
|
|
||||||
this._balances_by_internal_index[c] = {
|
|
||||||
c: balances.addresses[addr].confirmed,
|
|
||||||
u: balances.addresses[addr].unconfirmed,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this._lastBalanceFetch = +new Date();
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchUtxo() {
|
|
||||||
// considering only confirmed balance
|
|
||||||
let addressess = [];
|
|
||||||
|
|
||||||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
|
|
||||||
if (this._balances_by_external_index[c] && this._balances_by_external_index[c].c && this._balances_by_external_index[c].c > 0) {
|
|
||||||
addressess.push(this._getExternalAddressByIndex(c));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
|
|
||||||
if (this._balances_by_internal_index[c] && this._balances_by_internal_index[c].c && this._balances_by_internal_index[c].c > 0) {
|
|
||||||
addressess.push(this._getInternalAddressByIndex(c));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this._utxo = [];
|
|
||||||
for (let arr of Object.values(await BlueElectrum.multiGetUtxoByAddress(addressess))) {
|
|
||||||
this._utxo = this._utxo.concat(arr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getUtxo() {
|
|
||||||
return this._utxo;
|
|
||||||
}
|
|
||||||
|
|
||||||
_getDerivationPathByAddress(address) {
|
|
||||||
const path = "m/84'/0'/0'";
|
|
||||||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
|
|
||||||
if (this._getExternalAddressByIndex(c) === address) return path + '/0/' + c;
|
|
||||||
}
|
|
||||||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
|
|
||||||
if (this._getInternalAddressByIndex(c) === address) return path + '/1/' + c;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_getPubkeyByAddress(address) {
|
|
||||||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
|
|
||||||
if (this._getExternalAddressByIndex(c) === address) return this._getNodePubkeyByIndex(0, c);
|
|
||||||
}
|
|
||||||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
|
|
||||||
if (this._getInternalAddressByIndex(c) === address) return this._getNodePubkeyByIndex(1, c);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 + this.gap_limit; c++) {
|
|
||||||
if (this._getInternalAddressByIndex(c) === address) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated
|
|
||||||
*/
|
|
||||||
createTx(utxos, amount, fee, address) {
|
|
||||||
throw new Error('Deprecated');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param utxos {Array.<{vout: Number, value: Number, txId: String, address: String}>} List of spendable utxos
|
|
||||||
* @param targets {Array.<{value: Number, address: String}>} Where coins are going. If theres only 1 target and that target has no value - this will send MAX to that address (respecting fee rate)
|
|
||||||
* @param feeRate {Number} satoshi per byte
|
|
||||||
* @param changeAddress {String} Excessive coins will go back to that address
|
|
||||||
* @param sequence {Number} Used in RBF
|
|
||||||
* @param skipSigning {boolean} Whether we should skip signing, use returned `psbt` in that case
|
|
||||||
* @returns {{outputs: Array, tx: Transaction, inputs: Array, fee: Number, psbt: Psbt}}
|
|
||||||
*/
|
|
||||||
createTransaction(utxos, targets, feeRate, changeAddress, sequence, skipSigning = false) {
|
|
||||||
if (!changeAddress) throw new Error('No change address provided');
|
|
||||||
sequence = sequence || HDSegwitBech32Wallet.defaultRBFSequence;
|
|
||||||
|
|
||||||
let algo = coinSelectAccumulative;
|
|
||||||
if (targets.length === 1 && targets[0] && !targets[0].value) {
|
|
||||||
// we want to send MAX
|
|
||||||
algo = coinSelectSplit;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { inputs, outputs, fee } = algo(utxos, targets, feeRate);
|
|
||||||
|
|
||||||
// .inputs and .outputs will be undefined if no solution was found
|
|
||||||
if (!inputs || !outputs) {
|
|
||||||
throw new Error('Not enough balance. Try sending smaller amount');
|
|
||||||
}
|
|
||||||
|
|
||||||
let psbt = new bitcoin.Psbt();
|
|
||||||
|
|
||||||
let c = 0;
|
|
||||||
let keypairs = {};
|
|
||||||
let values = {};
|
|
||||||
|
|
||||||
inputs.forEach(input => {
|
|
||||||
let keyPair;
|
|
||||||
if (!skipSigning) {
|
|
||||||
// skiping signing related stuff
|
|
||||||
keyPair = bitcoin.ECPair.fromWIF(this._getWifForAddress(input.address));
|
|
||||||
keypairs[c] = keyPair;
|
|
||||||
}
|
|
||||||
values[c] = input.value;
|
|
||||||
c++;
|
|
||||||
if (!skipSigning) {
|
|
||||||
// skiping signing related stuff
|
|
||||||
if (!input.address || !this._getWifForAddress(input.address)) throw new Error('Internal error: no address or WIF to sign input');
|
|
||||||
}
|
|
||||||
let pubkey = this._getPubkeyByAddress(input.address);
|
|
||||||
let masterFingerprint = Buffer.from([0x00, 0x00, 0x00, 0x00]);
|
|
||||||
// this is not correct fingerprint, as we dont know real fingerprint - we got zpub with 84/0, but fingerpting
|
|
||||||
// should be from root. basically, fingerprint should be provided from outside by user when importing zpub
|
|
||||||
let path = this._getDerivationPathByAddress(input.address);
|
|
||||||
const p2wpkh = bitcoin.payments.p2wpkh({ pubkey });
|
|
||||||
psbt.addInput({
|
|
||||||
hash: input.txId,
|
|
||||||
index: input.vout,
|
|
||||||
sequence,
|
|
||||||
bip32Derivation: [
|
|
||||||
{
|
|
||||||
masterFingerprint,
|
|
||||||
path,
|
|
||||||
pubkey,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
witnessUtxo: {
|
|
||||||
script: p2wpkh.output,
|
|
||||||
value: input.value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
outputs.forEach(output => {
|
|
||||||
// if output has no address - this is change output
|
|
||||||
let change = false;
|
|
||||||
if (!output.address) {
|
|
||||||
change = true;
|
|
||||||
output.address = changeAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
let path = this._getDerivationPathByAddress(output.address);
|
|
||||||
let pubkey = this._getPubkeyByAddress(output.address);
|
|
||||||
let masterFingerprint = Buffer.from([0x00, 0x00, 0x00, 0x00]);
|
|
||||||
// this is not correct fingerprint, as we dont know realfingerprint - we got zpub with 84/0, but fingerpting
|
|
||||||
// should be from root. basically, fingerprint should be provided from outside by user when importing zpub
|
|
||||||
|
|
||||||
let outputData = {
|
|
||||||
address: output.address,
|
|
||||||
value: output.value,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (change) {
|
|
||||||
outputData['bip32Derivation'] = [
|
|
||||||
{
|
|
||||||
masterFingerprint,
|
|
||||||
path,
|
|
||||||
pubkey,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
psbt.addOutput(outputData);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!skipSigning) {
|
|
||||||
// skiping signing related stuff
|
|
||||||
for (let cc = 0; cc < c; cc++) {
|
|
||||||
psbt.signInput(cc, keypairs[cc]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let tx;
|
|
||||||
if (!skipSigning) {
|
|
||||||
tx = psbt.finalizeAllInputs().extractTransaction();
|
|
||||||
}
|
|
||||||
return { tx, inputs, outputs, fee, psbt };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Combines 2 PSBTs into final transaction from which you can
|
|
||||||
* get HEX and broadcast
|
|
||||||
*
|
|
||||||
* @param base64one {string}
|
|
||||||
* @param base64two {string}
|
|
||||||
* @returns {Transaction}
|
|
||||||
*/
|
|
||||||
combinePsbt(base64one, base64two) {
|
|
||||||
const final1 = bitcoin.Psbt.fromBase64(base64one);
|
|
||||||
const final2 = bitcoin.Psbt.fromBase64(base64two);
|
|
||||||
final1.combine(final2);
|
|
||||||
return final1.finalizeAllInputs().extractTransaction();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates Segwit Bech32 Bitcoin address
|
|
||||||
*
|
|
||||||
* @param hdNode
|
|
||||||
* @returns {String}
|
|
||||||
*/
|
|
||||||
static _nodeToBech32SegwitAddress(hdNode) {
|
|
||||||
return bitcoin.payments.p2wpkh({
|
|
||||||
pubkey: hdNode.publicKey,
|
|
||||||
}).address;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts zpub to xpub
|
|
||||||
*
|
|
||||||
* @param {String} zpub
|
|
||||||
* @returns {String} xpub
|
|
||||||
*/
|
|
||||||
static _zpubToXpub(zpub) {
|
|
||||||
let data = b58.decode(zpub);
|
|
||||||
data = data.slice(4);
|
|
||||||
data = Buffer.concat([Buffer.from('0488b21e', 'hex'), data]);
|
|
||||||
|
|
||||||
return b58.encode(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
static _getTransactionsFromHistories(histories) {
|
|
||||||
let txs = [];
|
|
||||||
for (let history of Object.values(histories)) {
|
|
||||||
for (let tx of history) {
|
|
||||||
txs.push(tx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return txs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Broadcast txhex. Can throw an exception if failed
|
|
||||||
*
|
|
||||||
* @param {String} txhex
|
|
||||||
* @returns {Promise<boolean>}
|
|
||||||
*/
|
|
||||||
async broadcastTx(txhex) {
|
|
||||||
let broadcast = await BlueElectrum.broadcastV2(txhex);
|
|
||||||
console.log({ broadcast });
|
|
||||||
if (broadcast.indexOf('successfully') !== -1) return true;
|
|
||||||
return broadcast.length === 64; // this means return string is txid (precise length), so it was broadcasted ok
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,48 +1,18 @@
|
||||||
import { AbstractHDWallet } from './abstract-hd-wallet';
|
|
||||||
import Frisbee from 'frisbee';
|
|
||||||
import { NativeModules } from 'react-native';
|
|
||||||
import bip39 from 'bip39';
|
import bip39 from 'bip39';
|
||||||
import BigNumber from 'bignumber.js';
|
import BigNumber from 'bignumber.js';
|
||||||
import b58 from 'bs58check';
|
import b58 from 'bs58check';
|
||||||
import signer from '../models/signer';
|
import signer from '../models/signer';
|
||||||
import { BitcoinUnit } from '../models/bitcoinUnits';
|
import { BitcoinUnit } from '../models/bitcoinUnits';
|
||||||
|
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');
|
||||||
|
|
||||||
const { RNRandomBytes } = NativeModules;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts ypub to xpub
|
|
||||||
* @param {String} ypub - wallet ypub
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
function ypubToXpub(ypub) {
|
|
||||||
let data = b58.decode(ypub);
|
|
||||||
if (data.readUInt32BE() !== 0x049d7cb2) throw new Error('Not a valid ypub extended key!');
|
|
||||||
data = data.slice(4);
|
|
||||||
data = Buffer.concat([Buffer.from('0488b21e', 'hex'), data]);
|
|
||||||
|
|
||||||
return b58.encode(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates Segwit P2SH Bitcoin address
|
|
||||||
* @param hdNode
|
|
||||||
* @returns {String}
|
|
||||||
*/
|
|
||||||
function nodeToP2shSegwitAddress(hdNode) {
|
|
||||||
const { address } = bitcoin.payments.p2sh({
|
|
||||||
redeem: bitcoin.payments.p2wpkh({ pubkey: hdNode.publicKey }),
|
|
||||||
});
|
|
||||||
return address;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HD Wallet (BIP39).
|
* HD Wallet (BIP39).
|
||||||
* In particular, BIP49 (P2SH Segwit)
|
* In particular, BIP49 (P2SH Segwit)
|
||||||
* @see https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki
|
* @see https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki
|
||||||
*/
|
*/
|
||||||
export class HDSegwitP2SHWallet extends AbstractHDWallet {
|
export class HDSegwitP2SHWallet extends AbstractHDElectrumWallet {
|
||||||
static type = 'HDsegwitP2SH';
|
static type = 'HDsegwitP2SH';
|
||||||
static typeReadable = 'HD SegWit (BIP49 P2SH)';
|
static typeReadable = 'HD SegWit (BIP49 P2SH)';
|
||||||
|
|
||||||
|
@ -54,37 +24,6 @@ export class HDSegwitP2SHWallet extends AbstractHDWallet {
|
||||||
return true;
|
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
|
* Get internal/external WIF by wallet index
|
||||||
* @param {Boolean} internal
|
* @param {Boolean} internal
|
||||||
|
@ -107,11 +46,11 @@ export class HDSegwitP2SHWallet extends AbstractHDWallet {
|
||||||
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
|
||||||
|
|
||||||
if (!this._node0) {
|
if (!this._node0) {
|
||||||
const xpub = ypubToXpub(this.getXpub());
|
const xpub = this.constructor._ypubToXpub(this.getXpub());
|
||||||
const hdNode = HDNode.fromBase58(xpub);
|
const hdNode = HDNode.fromBase58(xpub);
|
||||||
this._node0 = hdNode.derive(0);
|
this._node0 = hdNode.derive(0);
|
||||||
}
|
}
|
||||||
const address = nodeToP2shSegwitAddress(this._node0.derive(index));
|
const address = this.constructor._nodeToP2shSegwitAddress(this._node0.derive(index));
|
||||||
|
|
||||||
return (this.external_addresses_cache[index] = address);
|
return (this.external_addresses_cache[index] = address);
|
||||||
}
|
}
|
||||||
|
@ -121,11 +60,11 @@ export class HDSegwitP2SHWallet extends AbstractHDWallet {
|
||||||
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
|
||||||
|
|
||||||
if (!this._node1) {
|
if (!this._node1) {
|
||||||
const xpub = ypubToXpub(this.getXpub());
|
const xpub = this.constructor._ypubToXpub(this.getXpub());
|
||||||
const hdNode = HDNode.fromBase58(xpub);
|
const hdNode = HDNode.fromBase58(xpub);
|
||||||
this._node1 = hdNode.derive(1);
|
this._node1 = hdNode.derive(1);
|
||||||
}
|
}
|
||||||
const address = nodeToP2shSegwitAddress(this._node1.derive(index));
|
const address = this.constructor._nodeToP2shSegwitAddress(this._node1.derive(index));
|
||||||
|
|
||||||
return (this.internal_addresses_cache[index] = address);
|
return (this.internal_addresses_cache[index] = address);
|
||||||
}
|
}
|
||||||
|
@ -158,108 +97,6 @@ export class HDSegwitP2SHWallet extends AbstractHDWallet {
|
||||||
return this._xpub;
|
return this._xpub;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _getTransactionsBatch(addresses) {
|
|
||||||
const api = new Frisbee({ baseURI: 'https://blockchain.info' });
|
|
||||||
let transactions = [];
|
|
||||||
let offset = 0;
|
|
||||||
|
|
||||||
while (1) {
|
|
||||||
let response = await api.get('/multiaddr?active=' + addresses + '&n=100&offset=' + offset);
|
|
||||||
|
|
||||||
if (response && response.body) {
|
|
||||||
if (response.body.txs && response.body.txs.length === 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._lastTxFetch = +new Date();
|
|
||||||
|
|
||||||
// processing TXs and adding to internal memory
|
|
||||||
if (response.body.txs) {
|
|
||||||
for (let tx of response.body.txs) {
|
|
||||||
let value = 0;
|
|
||||||
|
|
||||||
for (let input of tx.inputs) {
|
|
||||||
// ----- INPUTS
|
|
||||||
|
|
||||||
if (input.prev_out && input.prev_out.addr && this.weOwnAddress(input.prev_out.addr)) {
|
|
||||||
// this is outgoing from us
|
|
||||||
value -= input.prev_out.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let output of tx.out) {
|
|
||||||
// ----- OUTPUTS
|
|
||||||
|
|
||||||
if (output.addr && this.weOwnAddress(output.addr)) {
|
|
||||||
// this is incoming to us
|
|
||||||
value += output.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tx.value = value; // new BigNumber(value).div(100000000).toString() * 1;
|
|
||||||
if (response.body.hasOwnProperty('info')) {
|
|
||||||
if (response.body.info.latest_block.height && tx.block_height) {
|
|
||||||
tx.confirmations = response.body.info.latest_block.height - tx.block_height + 1;
|
|
||||||
} else {
|
|
||||||
tx.confirmations = 0;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tx.confirmations = 0;
|
|
||||||
}
|
|
||||||
transactions.push(tx);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.body.txs.length < 100) {
|
|
||||||
// this fetch yilded less than page size, thus requesting next batch makes no sense
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break; // error ?
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error('Could not fetch transactions from API: ' + response.err); // breaks here
|
|
||||||
}
|
|
||||||
|
|
||||||
offset += 100;
|
|
||||||
}
|
|
||||||
return transactions;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritDoc
|
|
||||||
*/
|
|
||||||
async fetchTransactions() {
|
|
||||||
try {
|
|
||||||
if (this.usedAddresses.length === 0) {
|
|
||||||
// just for any case, refresh balance (it refreshes internal `this.usedAddresses`)
|
|
||||||
await this.fetchBalance();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.transactions = [];
|
|
||||||
|
|
||||||
let addresses4batch = [];
|
|
||||||
for (let addr of this.usedAddresses) {
|
|
||||||
addresses4batch.push(addr);
|
|
||||||
if (addresses4batch.length >= 45) {
|
|
||||||
let addresses = addresses4batch.join('|');
|
|
||||||
let transactions = await this._getTransactionsBatch(addresses);
|
|
||||||
this.transactions = this.transactions.concat(transactions);
|
|
||||||
addresses4batch = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// final batch
|
|
||||||
for (let c = 0; c <= this.gap_limit; c++) {
|
|
||||||
addresses4batch.push(this._getExternalAddressByIndex(this.next_free_address_index + c));
|
|
||||||
addresses4batch.push(this._getInternalAddressByIndex(this.next_free_change_address_index + c));
|
|
||||||
}
|
|
||||||
let addresses = addresses4batch.join('|');
|
|
||||||
let transactions = await this._getTransactionsBatch(addresses);
|
|
||||||
this.transactions = this.transactions.concat(transactions);
|
|
||||||
} catch (err) {
|
|
||||||
console.warn(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param utxos
|
* @param utxos
|
||||||
|
@ -291,4 +128,30 @@ export class HDSegwitP2SHWallet extends AbstractHDWallet {
|
||||||
this._getInternalAddressByIndex(this.next_free_change_address_index),
|
this._getInternalAddressByIndex(this.next_free_change_address_index),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts ypub to xpub
|
||||||
|
* @param {String} ypub - wallet ypub
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
static _ypubToXpub(ypub) {
|
||||||
|
let data = b58.decode(ypub);
|
||||||
|
if (data.readUInt32BE() !== 0x049d7cb2) throw new Error('Not a valid ypub extended key!');
|
||||||
|
data = data.slice(4);
|
||||||
|
data = Buffer.concat([Buffer.from('0488b21e', 'hex'), data]);
|
||||||
|
|
||||||
|
return b58.encode(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates Segwit P2SH Bitcoin address
|
||||||
|
* @param hdNode
|
||||||
|
* @returns {String}
|
||||||
|
*/
|
||||||
|
static _nodeToP2shSegwitAddress(hdNode) {
|
||||||
|
const { address } = bitcoin.payments.p2sh({
|
||||||
|
redeem: bitcoin.payments.p2wpkh({ pubkey: hdNode.publicKey }),
|
||||||
|
});
|
||||||
|
return address;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,7 +55,7 @@ it('can create a Segwit HD (BIP49)', async function() {
|
||||||
assert.ok(hd._lastTxFetch === 0);
|
assert.ok(hd._lastTxFetch === 0);
|
||||||
await hd.fetchTransactions();
|
await hd.fetchTransactions();
|
||||||
assert.ok(hd._lastTxFetch > 0);
|
assert.ok(hd._lastTxFetch > 0);
|
||||||
assert.strictEqual(hd.transactions.length, 4);
|
assert.strictEqual(hd.getTransactions().length, 4);
|
||||||
|
|
||||||
assert.strictEqual('L4MqtwJm6hkbACLG4ho5DF8GhcXdLEbbvpJnbzA9abfD6RDpbr2m', hd._getExternalWIFByIndex(0));
|
assert.strictEqual('L4MqtwJm6hkbACLG4ho5DF8GhcXdLEbbvpJnbzA9abfD6RDpbr2m', hd._getExternalWIFByIndex(0));
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
|
@ -84,7 +84,7 @@ it('HD (BIP49) can work with a gap', async function() {
|
||||||
// console.log('external', c, hd._getExternalAddressByIndex(c));
|
// console.log('external', c, hd._getExternalAddressByIndex(c));
|
||||||
// }
|
// }
|
||||||
await hd.fetchTransactions();
|
await hd.fetchTransactions();
|
||||||
assert.ok(hd.transactions.length >= 3);
|
assert.ok(hd.getTransactions().length >= 3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Segwit HD (BIP49) can batch fetch many txs', async function() {
|
it('Segwit HD (BIP49) can batch fetch many txs', async function() {
|
||||||
|
@ -93,7 +93,7 @@ it('Segwit HD (BIP49) can batch fetch many txs', async function() {
|
||||||
hd._xpub = 'ypub6WZ2c7YJ1SQ1rBYftwMqwV9bBmybXzETFxWmkzMz25bCf6FkDdXjNgR7zRW8JGSnoddNdUH7ZQS7JeQAddxdGpwgPskcsXFcvSn1JdGVcPQ';
|
hd._xpub = 'ypub6WZ2c7YJ1SQ1rBYftwMqwV9bBmybXzETFxWmkzMz25bCf6FkDdXjNgR7zRW8JGSnoddNdUH7ZQS7JeQAddxdGpwgPskcsXFcvSn1JdGVcPQ';
|
||||||
await hd.fetchBalance();
|
await hd.fetchBalance();
|
||||||
await hd.fetchTransactions();
|
await hd.fetchTransactions();
|
||||||
assert.ok(hd.getTransactions().length === 153);
|
assert.strictEqual(hd.getTransactions().length, 152);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Segwit HD (BIP49) can fetch more data if pointers to last_used_addr are lagging behind', async function() {
|
it('Segwit HD (BIP49) can fetch more data if pointers to last_used_addr are lagging behind', async function() {
|
||||||
|
@ -104,7 +104,7 @@ it('Segwit HD (BIP49) can fetch more data if pointers to last_used_addr are lagg
|
||||||
hd.next_free_address_index = 50;
|
hd.next_free_address_index = 50;
|
||||||
await hd.fetchBalance();
|
await hd.fetchBalance();
|
||||||
await hd.fetchTransactions();
|
await hd.fetchTransactions();
|
||||||
assert.strictEqual(hd.getTransactions().length, 153);
|
assert.strictEqual(hd.getTransactions().length, 152);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Segwit HD (BIP49) can generate addressess only via ypub', function() {
|
it('Segwit HD (BIP49) can generate addressess only via ypub', function() {
|
||||||
|
@ -143,9 +143,14 @@ it('HD (BIP49) can create TX', async () => {
|
||||||
hd.setSecret(process.env.HD_MNEMONIC_BIP49);
|
hd.setSecret(process.env.HD_MNEMONIC_BIP49);
|
||||||
assert.ok(hd.validateMnemonic());
|
assert.ok(hd.validateMnemonic());
|
||||||
|
|
||||||
|
await hd.fetchBalance();
|
||||||
await hd.fetchUtxo();
|
await hd.fetchUtxo();
|
||||||
await hd.getChangeAddressAsync(); // to refresh internal pointer to next free address
|
assert.ok(typeof hd.utxo[0].confirmations === 'number');
|
||||||
await hd.getAddressAsync(); // to refresh internal pointer to next free address
|
assert.ok(hd.utxo[0].txid);
|
||||||
|
assert.ok(hd.utxo[0].vout !== undefined);
|
||||||
|
assert.ok(hd.utxo[0].amount);
|
||||||
|
assert.ok(hd.utxo[0].address);
|
||||||
|
assert.ok(hd.utxo[0].wif);
|
||||||
let txhex = hd.createTx(hd.utxo, 0.000014, 0.000001, '3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK');
|
let txhex = hd.createTx(hd.utxo, 0.000014, 0.000001, '3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK');
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
txhex,
|
txhex,
|
||||||
|
@ -209,21 +214,6 @@ it('HD (BIP49) can create TX', async () => {
|
||||||
assert.strictEqual(tx.outs[0].value, 75000);
|
assert.strictEqual(tx.outs[0].value, 75000);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Segwit HD (BIP49) can fetch UTXO', async function() {
|
|
||||||
let hd = new HDSegwitP2SHWallet();
|
|
||||||
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('Segwit HD (BIP49) can fetch balance with many used addresses in hierarchy', async function() {
|
it('Segwit HD (BIP49) can fetch balance with many used addresses in hierarchy', async function() {
|
||||||
if (!process.env.HD_MNEMONIC_BIP49_MANY_TX) {
|
if (!process.env.HD_MNEMONIC_BIP49_MANY_TX) {
|
||||||
console.error('process.env.HD_MNEMONIC_BIP49_MANY_TX not set, skipped');
|
console.error('process.env.HD_MNEMONIC_BIP49_MANY_TX not set, skipped');
|
||||||
|
@ -302,7 +292,7 @@ it('can create a Legacy HD (BIP44)', async function() {
|
||||||
assert.ok(hd._lastTxFetch === 0);
|
assert.ok(hd._lastTxFetch === 0);
|
||||||
await hd.fetchTransactions();
|
await hd.fetchTransactions();
|
||||||
assert.ok(hd._lastTxFetch > 0);
|
assert.ok(hd._lastTxFetch > 0);
|
||||||
assert.strictEqual(hd.transactions.length, 4);
|
assert.strictEqual(hd.getTransactions().length, 4);
|
||||||
assert.strictEqual(hd.next_free_address_index, 1);
|
assert.strictEqual(hd.next_free_address_index, 1);
|
||||||
assert.strictEqual(hd.next_free_change_address_index, 1);
|
assert.strictEqual(hd.next_free_change_address_index, 1);
|
||||||
|
|
||||||
|
@ -403,7 +393,7 @@ it.skip('HD breadwallet works', async function() {
|
||||||
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.transactions.length, 177);
|
assert.strictEqual(hdBread.getTransactions().length, 177);
|
||||||
for (let tx of hdBread.getTransactions()) {
|
for (let tx of hdBread.getTransactions()) {
|
||||||
assert.ok(tx.confirmations);
|
assert.ok(tx.confirmations);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue