2018-07-21 14:52:54 +02:00
|
|
|
import { AbstractHDWallet } from './abstract-hd-wallet';
|
2018-06-25 00:19:27 +02:00
|
|
|
import Frisbee from 'frisbee';
|
2018-12-11 23:52:46 +01:00
|
|
|
import { NativeModules } from 'react-native';
|
2019-01-02 17:15:55 +01:00
|
|
|
import bip39 from 'bip39';
|
|
|
|
import BigNumber from 'bignumber.js';
|
|
|
|
import b58 from 'bs58check';
|
|
|
|
import signer from '../models/signer';
|
2019-08-04 21:33:15 +02:00
|
|
|
import { BitcoinUnit } from '../models/bitcoinUnits';
|
2019-05-22 01:00:03 +02:00
|
|
|
const bitcoin = require('bitcoinjs-lib');
|
|
|
|
const bitcoin5 = require('bitcoinjs5');
|
|
|
|
const HDNode = require('bip32');
|
2019-01-02 17:15:55 +01:00
|
|
|
|
2018-12-11 23:52:46 +01:00
|
|
|
const { RNRandomBytes } = NativeModules;
|
2018-06-25 00:19:27 +02:00
|
|
|
|
2019-01-02 16:57:47 +01:00
|
|
|
/**
|
|
|
|
* Converts ypub to xpub
|
|
|
|
* @param {String} ypub - wallet ypub
|
|
|
|
* @returns {*}
|
|
|
|
*/
|
2019-01-02 17:15:55 +01:00
|
|
|
function ypubToXpub(ypub) {
|
2019-01-02 16:57:47 +01:00
|
|
|
let data = b58.decode(ypub);
|
|
|
|
data = data.slice(4);
|
2019-01-02 17:15:55 +01:00
|
|
|
data = Buffer.concat([Buffer.from('0488b21e', 'hex'), data]);
|
|
|
|
|
2019-01-02 16:57:47 +01:00
|
|
|
return b58.encode(data);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates Segwit P2SH Bitcoin address
|
|
|
|
* @param hdNode
|
|
|
|
* @returns {String}
|
|
|
|
*/
|
2019-01-02 17:15:55 +01:00
|
|
|
function nodeToP2shSegwitAddress(hdNode) {
|
2019-05-22 01:00:03 +02:00
|
|
|
const { address } = bitcoin5.payments.p2sh({
|
|
|
|
redeem: bitcoin5.payments.p2wpkh({ pubkey: hdNode.publicKey }),
|
|
|
|
});
|
|
|
|
return address;
|
2019-01-02 16:57:47 +01:00
|
|
|
}
|
|
|
|
|
2018-06-25 00:19:27 +02:00
|
|
|
/**
|
|
|
|
* HD Wallet (BIP39).
|
2018-07-07 23:15:14 +02:00
|
|
|
* In particular, BIP49 (P2SH Segwit)
|
|
|
|
* @see https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki
|
2018-06-25 00:19:27 +02:00
|
|
|
*/
|
2018-07-21 14:52:54 +02:00
|
|
|
export class HDSegwitP2SHWallet extends AbstractHDWallet {
|
2018-12-28 16:52:06 +01:00
|
|
|
static type = 'HDsegwitP2SH';
|
|
|
|
static typeReadable = 'HD SegWit (BIP49 P2SH)';
|
2018-06-25 00:19:27 +02:00
|
|
|
|
2018-09-01 01:28:19 +02:00
|
|
|
allowSend() {
|
2018-10-06 02:45:24 +02:00
|
|
|
return true;
|
2018-09-01 01:28:19 +02:00
|
|
|
}
|
|
|
|
|
2019-08-04 21:33:15 +02:00
|
|
|
allowSendMax(): boolean {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2018-12-11 23:52:46 +01:00
|
|
|
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();
|
|
|
|
});
|
2018-07-28 22:19:11 +02:00
|
|
|
}
|
2018-12-11 23:52:46 +01:00
|
|
|
|
|
|
|
// 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();
|
|
|
|
});
|
|
|
|
});
|
2018-07-28 22:19:11 +02:00
|
|
|
}
|
|
|
|
|
2018-06-25 00:19:27 +02:00
|
|
|
_getExternalWIFByIndex(index) {
|
2019-01-02 17:08:54 +01:00
|
|
|
return this._getWIFByIndex(false, index);
|
2018-06-25 00:19:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
_getInternalWIFByIndex(index) {
|
2019-01-02 17:08:54 +01:00
|
|
|
return this._getWIFByIndex(true, index);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get internal/external WIF by wallet index
|
|
|
|
* @param {Boolean} internal
|
|
|
|
* @param {Number} index
|
|
|
|
* @returns {*}
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
_getWIFByIndex(internal, index) {
|
2019-01-02 17:15:55 +01:00
|
|
|
const mnemonic = this.secret;
|
|
|
|
const seed = bip39.mnemonicToSeed(mnemonic);
|
|
|
|
const root = bitcoin.HDNode.fromSeedBuffer(seed);
|
|
|
|
const path = `m/49'/0'/0'/${internal ? 1 : 0}/${index}`;
|
|
|
|
const child = root.derivePath(path);
|
2019-01-02 17:08:54 +01:00
|
|
|
|
2018-06-25 00:19:27 +02:00
|
|
|
return child.keyPair.toWIF();
|
|
|
|
}
|
|
|
|
|
2019-01-02 17:15:55 +01:00
|
|
|
_getExternalAddressByIndex(index) {
|
2018-06-25 00:19:27 +02:00
|
|
|
index = index * 1; // cast to int
|
2018-07-22 16:49:59 +02:00
|
|
|
if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit
|
2019-01-02 16:57:47 +01:00
|
|
|
|
2019-05-21 00:57:46 +02:00
|
|
|
if (!this._node0) {
|
|
|
|
const xpub = ypubToXpub(this.getXpub());
|
2019-05-22 01:00:03 +02:00
|
|
|
const hdNode = HDNode.fromBase58(xpub);
|
2019-05-21 00:57:46 +02:00
|
|
|
this._node0 = hdNode.derive(0);
|
|
|
|
}
|
|
|
|
const address = nodeToP2shSegwitAddress(this._node0.derive(index));
|
2019-01-02 16:57:47 +01:00
|
|
|
|
|
|
|
return (this.external_addresses_cache[index] = address);
|
2018-06-25 00:19:27 +02:00
|
|
|
}
|
|
|
|
|
2019-01-02 17:15:55 +01:00
|
|
|
_getInternalAddressByIndex(index) {
|
2018-06-25 00:19:27 +02:00
|
|
|
index = index * 1; // cast to int
|
2018-07-22 16:49:59 +02:00
|
|
|
if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit
|
2019-01-02 16:57:47 +01:00
|
|
|
|
2019-05-21 00:57:46 +02:00
|
|
|
if (!this._node1) {
|
|
|
|
const xpub = ypubToXpub(this.getXpub());
|
2019-05-22 01:00:03 +02:00
|
|
|
const hdNode = HDNode.fromBase58(xpub);
|
2019-05-21 00:57:46 +02:00
|
|
|
this._node1 = hdNode.derive(1);
|
|
|
|
}
|
|
|
|
const address = nodeToP2shSegwitAddress(this._node1.derive(index));
|
2019-01-02 16:57:47 +01:00
|
|
|
|
|
|
|
return (this.internal_addresses_cache[index] = address);
|
2018-06-25 00:19:27 +02:00
|
|
|
}
|
|
|
|
|
2018-07-08 16:32:38 +02:00
|
|
|
/**
|
|
|
|
* Returning ypub actually, not xpub. Keeping same method name
|
|
|
|
* for compatibility.
|
|
|
|
*
|
|
|
|
* @return {String} ypub
|
|
|
|
*/
|
2018-07-07 23:15:14 +02:00
|
|
|
getXpub() {
|
2018-07-22 16:49:59 +02:00
|
|
|
if (this._xpub) {
|
|
|
|
return this._xpub; // cache hit
|
|
|
|
}
|
2018-07-08 16:32:38 +02:00
|
|
|
// first, getting xpub
|
2019-01-02 17:15:55 +01:00
|
|
|
const mnemonic = this.secret;
|
|
|
|
const seed = bip39.mnemonicToSeed(mnemonic);
|
2019-05-22 01:00:03 +02:00
|
|
|
const root = HDNode.fromSeed(seed);
|
2018-07-07 23:15:14 +02:00
|
|
|
|
2019-01-02 17:15:55 +01:00
|
|
|
const path = "m/49'/0'/0'";
|
|
|
|
const child = root.derivePath(path).neutered();
|
|
|
|
const xpub = child.toBase58();
|
2018-07-08 16:32:38 +02:00
|
|
|
|
|
|
|
// bitcoinjs does not support ypub yet, so we just convert it from xpub
|
|
|
|
let data = b58.decode(xpub);
|
|
|
|
data = data.slice(4);
|
|
|
|
data = Buffer.concat([Buffer.from('049d7cb2', 'hex'), data]);
|
2018-07-22 16:49:59 +02:00
|
|
|
this._xpub = b58.encode(data);
|
2019-01-02 17:15:55 +01:00
|
|
|
|
2018-07-22 16:49:59 +02:00
|
|
|
return this._xpub;
|
2018-07-07 23:15:14 +02:00
|
|
|
}
|
|
|
|
|
2019-03-31 01:06:03 +01:00
|
|
|
async _getTransactionsBatch(addresses) {
|
|
|
|
const api = new Frisbee({ baseURI: 'https://blockchain.info' });
|
|
|
|
let transactions = [];
|
|
|
|
let offset = 0;
|
2018-06-25 00:19:27 +02:00
|
|
|
|
2019-03-31 01:06:03 +01:00
|
|
|
while (1) {
|
|
|
|
let response = await api.get('/multiaddr?active=' + addresses + '&n=100&offset=' + offset);
|
2018-07-21 14:52:54 +02:00
|
|
|
|
2019-03-31 01:06:03 +01:00
|
|
|
if (response && response.body) {
|
|
|
|
if (response.body.txs && response.body.txs.length === 0) {
|
|
|
|
break;
|
|
|
|
}
|
2018-06-25 00:19:27 +02:00
|
|
|
|
2019-03-31 01:06:03 +01:00
|
|
|
this._lastTxFetch = +new Date();
|
2018-07-28 22:19:11 +02:00
|
|
|
|
2019-03-31 01:06:03 +01:00
|
|
|
// processing TXs and adding to internal memory
|
|
|
|
if (response.body.txs) {
|
|
|
|
for (let tx of response.body.txs) {
|
|
|
|
let value = 0;
|
2018-07-22 16:49:59 +02:00
|
|
|
|
2019-03-31 01:06:03 +01:00
|
|
|
for (let input of tx.inputs) {
|
|
|
|
// ----- INPUTS
|
2018-07-22 16:49:59 +02:00
|
|
|
|
2019-03-31 01:06:03 +01:00
|
|
|
if (input.prev_out && input.prev_out.addr && this.weOwnAddress(input.prev_out.addr)) {
|
|
|
|
// this is outgoing from us
|
|
|
|
value -= input.prev_out.value;
|
2018-06-25 00:19:27 +02:00
|
|
|
}
|
2019-03-31 01:06:03 +01:00
|
|
|
}
|
2018-06-25 00:19:27 +02:00
|
|
|
|
2019-03-31 01:06:03 +01:00
|
|
|
for (let output of tx.out) {
|
|
|
|
// ----- OUTPUTS
|
2018-07-22 16:49:59 +02:00
|
|
|
|
2019-03-31 01:06:03 +01:00
|
|
|
if (output.addr && this.weOwnAddress(output.addr)) {
|
|
|
|
// this is incoming to us
|
|
|
|
value += output.value;
|
2018-06-25 00:19:27 +02:00
|
|
|
}
|
2019-03-31 01:06:03 +01:00
|
|
|
}
|
2018-06-25 00:19:27 +02:00
|
|
|
|
2019-03-31 01:06:03 +01:00
|
|
|
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;
|
2018-10-01 07:40:45 +02:00
|
|
|
} else {
|
2018-10-01 07:43:21 +02:00
|
|
|
tx.confirmations = 0;
|
2018-10-01 07:40:45 +02:00
|
|
|
}
|
2019-03-31 01:06:03 +01:00
|
|
|
} else {
|
|
|
|
tx.confirmations = 0;
|
2018-07-22 16:49:59 +02:00
|
|
|
}
|
2019-03-31 01:06:03 +01:00
|
|
|
transactions.push(tx);
|
|
|
|
}
|
2018-06-25 00:19:27 +02:00
|
|
|
|
2019-03-31 01:06:03 +01:00
|
|
|
if (response.body.txs.length < 100) {
|
|
|
|
// this fetch yilded less than page size, thus requesting next batch makes no sense
|
|
|
|
break;
|
2018-06-25 00:19:27 +02:00
|
|
|
}
|
|
|
|
} else {
|
2019-03-31 01:06:03 +01:00
|
|
|
break; // error ?
|
2018-06-25 00:19:27 +02:00
|
|
|
}
|
2019-03-31 01:06:03 +01:00
|
|
|
} else {
|
|
|
|
throw new Error('Could not fetch transactions from API: ' + response.err); // breaks here
|
|
|
|
}
|
|
|
|
|
|
|
|
offset += 100;
|
|
|
|
}
|
|
|
|
return transactions;
|
|
|
|
}
|
2018-07-22 16:49:59 +02:00
|
|
|
|
2019-03-31 01:06:03 +01:00
|
|
|
/**
|
|
|
|
* @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));
|
2018-06-25 00:19:27 +02:00
|
|
|
}
|
2019-03-31 01:06:03 +01:00
|
|
|
let addresses = addresses4batch.join('|');
|
|
|
|
let transactions = await this._getTransactionsBatch(addresses);
|
|
|
|
this.transactions = this.transactions.concat(transactions);
|
2018-07-22 16:49:59 +02:00
|
|
|
} catch (err) {
|
|
|
|
console.warn(err);
|
|
|
|
}
|
|
|
|
}
|
2018-06-25 00:19:27 +02:00
|
|
|
|
2019-08-04 21:33:15 +02:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param utxos
|
|
|
|
* @param amount Either float (BTC) or string 'MAX' (BitcoinUnit.MAX) to send all
|
|
|
|
* @param fee
|
|
|
|
* @param address
|
|
|
|
* @returns {string}
|
|
|
|
*/
|
2018-08-14 01:02:50 +02:00
|
|
|
createTx(utxos, amount, fee, address) {
|
2018-08-08 02:05:34 +02:00
|
|
|
for (let utxo of utxos) {
|
|
|
|
utxo.wif = this._getWifForAddress(utxo.address);
|
|
|
|
}
|
|
|
|
|
2018-10-20 23:10:21 +02:00
|
|
|
let amountPlusFee = parseFloat(new BigNumber(amount).plus(fee).toString(10));
|
2019-08-04 21:33:15 +02:00
|
|
|
|
|
|
|
if (amount === BitcoinUnit.MAX) {
|
|
|
|
amountPlusFee = new BigNumber(0);
|
|
|
|
for (let utxo of utxos) {
|
|
|
|
amountPlusFee = amountPlusFee.plus(utxo.amount);
|
|
|
|
}
|
|
|
|
amountPlusFee = amountPlusFee.dividedBy(100000000).toString(10);
|
|
|
|
}
|
|
|
|
|
2018-08-08 02:05:34 +02:00
|
|
|
return signer.createHDSegwitTransaction(
|
|
|
|
utxos,
|
|
|
|
address,
|
|
|
|
amountPlusFee,
|
|
|
|
fee,
|
|
|
|
this._getInternalAddressByIndex(this.next_free_change_address_index),
|
|
|
|
);
|
|
|
|
}
|
2018-06-25 00:19:27 +02:00
|
|
|
}
|