BlueWallet/class/abstract-hd-wallet.js

429 lines
14 KiB
JavaScript
Raw Normal View History

2018-07-21 14:52:54 +02:00
import { LegacyWallet } from './legacy-wallet';
import Frisbee from 'frisbee';
import { WatchOnlyWallet } from './watch-only-wallet';
2018-07-21 14:52:54 +02:00
const bip39 = require('bip39');
2018-12-22 03:46:35 +01:00
const BigNumber = require('bignumber.js');
const bitcoin = require('bitcoinjs-lib');
2018-07-21 14:52:54 +02:00
export class AbstractHDWallet extends LegacyWallet {
constructor() {
super();
this.type = 'abstract';
this.next_free_address_index = 0;
this.next_free_change_address_index = 0;
this.internal_addresses_cache = {}; // index => address
this.external_addresses_cache = {}; // index => address
this._xpub = ''; // cache
2018-08-08 02:05:34 +02:00
this.usedAddresses = [];
this._address_to_wif_cache = {};
2018-07-21 14:52:54 +02:00
}
generate() {
throw new Error('Not implemented');
}
2018-07-21 14:52:54 +02:00
allowSend() {
2018-08-08 02:05:34 +02:00
return false;
2018-07-21 14:52:54 +02:00
}
getTransactions() {
// need to reformat txs, as we are expected to return them in blockcypher format,
// but they are from blockchain.info actually (for all hd wallets)
let txs = [];
for (let tx of this.transactions) {
txs.push(AbstractHDWallet.convertTx(tx));
}
return txs;
}
static convertTx(tx) {
// console.log('converting', tx);
var clone = Object.assign({}, tx);
clone.received = new Date(clone.time * 1000).toISOString();
clone.outputs = clone.out;
2018-10-01 07:24:34 +02:00
if (clone.confirmations === undefined) {
clone.confirmations = 0;
}
for (let o of clone.outputs) {
o.addresses = [o.addr];
}
for (let i of clone.inputs) {
if (i.prev_out && i.prev_out.addr) {
i.addresses = [i.prev_out.addr];
}
}
if (!clone.value) {
let value = 0;
for (let inp of clone.inputs) {
if (inp.prev_out && inp.prev_out.xpub) {
// our owned
value -= inp.prev_out.value;
}
}
for (let out of clone.out) {
if (out.xpub) {
// to us
value += out.value;
}
}
clone.value = value;
}
return clone;
}
2018-07-21 14:52:54 +02:00
setSecret(newSecret) {
this.secret = newSecret.trim().toLowerCase();
2018-07-21 14:52:54 +02:00
this.secret = this.secret.replace(/[^a-zA-Z0-9]/g, ' ').replace(/\s+/g, ' ');
return this;
}
/**
* @return {Boolean} is mnemonic in `this.secret` valid
*/
2018-07-21 14:52:54 +02:00
validateMnemonic() {
return bip39.validateMnemonic(this.secret);
}
getMnemonicToSeedHex() {
return bip39.mnemonicToSeedHex(this.secret);
}
getTypeReadable() {
throw new Error('Not implemented');
2018-07-21 14:52:54 +02:00
}
/**
* Derives from hierarchy, returns next free address
* (the one that has no transactions). Looks for several,
* gives up if none found, and returns the used one
*
* @return {Promise.<string>}
*/
async getAddressAsync() {
// looking for free external address
let freeAddress = '';
let c;
2018-08-08 02:05:34 +02:00
for (c = 0; c < Math.max(5, this.usedAddresses.length); c++) {
if (this.next_free_address_index + c < 0) continue;
let address = this._getExternalAddressByIndex(this.next_free_address_index + c);
2018-08-01 00:37:35 +02:00
this.external_addresses_cache[this.next_free_address_index + c] = address; // updating cache just for any case
let WatchWallet = new WatchOnlyWallet();
WatchWallet.setSecret(address);
await WatchWallet.fetchTransactions();
if (WatchWallet.transactions.length === 0) {
// found free address
freeAddress = WatchWallet.getAddress();
this.next_free_address_index += c; // now points to _this one_
break;
}
}
if (!freeAddress) {
// could not find in cycle above, give up
freeAddress = this._getExternalAddressByIndex(this.next_free_address_index + c); // we didnt check this one, maybe its free
this.next_free_address_index += c + 1; // now points to the one _after_
}
return freeAddress;
}
2018-08-01 00:37:35 +02:00
/**
* Derives from hierarchy, returns next free CHANGE address
* (the one that has no transactions). Looks for several,
* gives up if none found, and returns the used one
*
* @return {Promise.<string>}
*/
async getChangeAddressAsync() {
// looking for free internal address
let freeAddress = '';
let c;
2018-08-08 02:05:34 +02:00
for (c = 0; c < Math.max(5, this.usedAddresses.length); c++) {
2018-08-01 00:37:35 +02:00
if (this.next_free_change_address_index + c < 0) continue;
let address = this._getInternalAddressByIndex(this.next_free_change_address_index + c);
this.internal_addresses_cache[this.next_free_change_address_index + c] = address; // updating cache just for any case
let WatchWallet = new WatchOnlyWallet();
WatchWallet.setSecret(address);
await WatchWallet.fetchTransactions();
if (WatchWallet.transactions.length === 0) {
// found free address
freeAddress = WatchWallet.getAddress();
this.next_free_change_address_index += c; // now points to _this one_
break;
}
}
if (!freeAddress) {
// could not find in cycle above, give up
freeAddress = this._getExternalAddressByIndex(this.next_free_address_index + c); // we didnt check this one, maybe its free
this.next_free_address_index += c + 1; // now points to the one _after_
}
return freeAddress;
}
/**
* Should not be used in HD wallets
*
* @deprecated
* @return {string}
*/
getAddress() {
return '';
2018-07-21 14:52:54 +02:00
}
_getExternalWIFByIndex(index) {
throw new Error('Not implemented');
}
_getInternalWIFByIndex(index) {
throw new Error('Not implemented');
}
_getExternalAddressByIndex(index) {
throw new Error('Not implemented');
}
_getInternalAddressByIndex(index) {
throw new Error('Not implemented');
}
getXpub() {
throw new Error('Not implemented');
}
/**
* Async function to fetch all transactions. Use getter to get actual txs.
* Also, sets internals:
* `this.internal_addresses_cache`
* `this.external_addresses_cache`
*
* @returns {Promise<void>}
*/
async fetchTransactions() {
try {
const api = new Frisbee({ baseURI: 'https://blockchain.info' });
this.transactions = [];
let offset = 0;
2018-07-21 14:52:54 +02:00
while (1) {
let response = await api.get('/multiaddr?active=' + this.getXpub() + '&n=100&offset=' + offset);
2018-07-21 14:52:54 +02:00
if (response && response.body) {
if (response.body.txs && response.body.txs.length === 0) {
break;
}
2018-07-21 14:52:54 +02:00
let latestBlock = false;
if (response.body.info && response.body.info.latest_block) {
latestBlock = response.body.info.latest_block.height;
}
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.xpub) {
// sent FROM US
value -= input.prev_out.value;
// setting internal caches to help ourselves in future...
let path = input.prev_out.xpub.path.split('/');
if (path[path.length - 2] === '1') {
// change address
this.next_free_change_address_index = Math.max(path[path.length - 1] * 1 + 1, this.next_free_change_address_index);
// setting to point to last maximum known change address + 1
}
if (path[path.length - 2] === '0') {
// main (aka external) address
this.next_free_address_index = Math.max(path[path.length - 1] * 1 + 1, this.next_free_address_index);
// setting to point to last maximum known main address + 1
}
// done with cache
2018-07-21 14:52:54 +02:00
}
}
for (let output of tx.out) {
// ----- OUTPUTS
if (output.xpub) {
// sent TO US (change)
value += output.value;
// setting internal caches to help ourselves in future...
let path = output.xpub.path.split('/');
if (path[path.length - 2] === '1') {
// change address
this.next_free_change_address_index = Math.max(path[path.length - 1] * 1 + 1, this.next_free_change_address_index);
// setting to point to last maximum known change address + 1
}
if (path[path.length - 2] === '0') {
// main (aka external) address
this.next_free_address_index = Math.max(path[path.length - 1] * 1 + 1, this.next_free_address_index);
// setting to point to last maximum known main address + 1
}
// done with cache
2018-07-21 14:52:54 +02:00
}
}
tx.value = value; // new BigNumber(value).div(100000000).toString() * 1;
if (!tx.confirmations && latestBlock) {
tx.confirmations = latestBlock - tx.block_height + 1;
}
2018-07-21 14:52:54 +02:00
this.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 ?
2018-07-21 14:52:54 +02:00
}
} else {
throw new Error('Could not fetch transactions from API: ' + response.err); // breaks here
2018-07-21 14:52:54 +02:00
}
offset += 100;
}
} catch (err) {
console.warn(err);
2018-07-21 14:52:54 +02:00
}
}
2018-08-08 02:05:34 +02:00
/**
* Given that `address` is in our HD hierarchy, try to find
* corresponding WIF
*
* @param address {String} In our HD hierarchy
* @return {String} WIF if found
*/
_getWifForAddress(address) {
if (this._address_to_wif_cache[address]) return this._address_to_wif_cache[address]; // cache hit
// fast approach, first lets iterate over all addressess we have in cache
for (let index of Object.keys(this.internal_addresses_cache)) {
if (this._getInternalAddressByIndex(index) === address) {
return (this._address_to_wif_cache[address] = this._getInternalWIFByIndex(index));
}
}
for (let index of Object.keys(this.external_addresses_cache)) {
if (this._getExternalAddressByIndex(index) === address) {
return (this._address_to_wif_cache[address] = this._getExternalWIFByIndex(index));
}
}
// no luck - lets iterate over all addressess we have up to first unused address index
for (let c = 0; c <= this.next_free_change_address_index + 3; c++) {
2018-08-08 02:05:34 +02:00
let possibleAddress = this._getInternalAddressByIndex(c);
if (possibleAddress === address) {
return (this._address_to_wif_cache[address] = this._getInternalWIFByIndex(c));
}
}
for (let c = 0; c <= this.next_free_address_index + 3; c++) {
2018-08-08 02:05:34 +02:00
let possibleAddress = this._getExternalAddressByIndex(c);
if (possibleAddress === address) {
return (this._address_to_wif_cache[address] = this._getExternalWIFByIndex(c));
}
}
throw new Error('Could not find WIF for ' + address);
}
createTx() {
throw new Error('Not implemented');
}
2018-12-22 03:46:35 +01:00
async fetchBalance() {
try {
const api = new Frisbee({ baseURI: 'https://www.blockonomics.co' });
let response = await api.post('/api/balance', { body: JSON.stringify({ addr: this.getXpub() }) });
if (response && response.body && response.body.response) {
this.balance = 0;
this.unconfirmed_balance = 0;
this.usedAddresses = [];
for (let addr of response.body.response) {
this.balance += addr.confirmed;
this.unconfirmed_balance += addr.unconfirmed;
this.usedAddresses.push(addr.addr);
}
this.balance = new BigNumber(this.balance).dividedBy(100000000).toString() * 1;
this.unconfirmed_balance = new BigNumber(this.unconfirmed_balance).dividedBy(100000000).toString() * 1;
this._lastBalanceFetch = +new Date();
} else {
throw new Error('Could not fetch balance from API: ' + response.err);
}
} catch (err) {
console.warn(err);
}
}
/**
* @inheritDoc
*/
async fetchUtxo() {
const api = new Frisbee({
baseURI: 'https://blockchain.info',
});
if (this.usedAddresses.length === 0) {
// just for any case, refresh balance (it refreshes internal `this.usedAddresses`)
await this.fetchBalance();
}
let addresses = this.usedAddresses.join('|');
addresses += '|' + this._getExternalAddressByIndex(this.next_free_address_index);
addresses += '|' + this._getInternalAddressByIndex(this.next_free_change_address_index);
let utxos = [];
let response;
try {
response = await api.get('/unspent?active=' + addresses + '&limit=1000');
// this endpoint does not support offset of some kind o_O
// so doing only one call
let json = response.body;
if (typeof json === 'undefined' || typeof json.unspent_outputs === 'undefined') {
throw new Error('Could not fetch UTXO from API ' + response.err);
}
for (let unspent of json.unspent_outputs) {
// a lil transform for signer module
unspent.txid = unspent.tx_hash_big_endian;
unspent.vout = unspent.tx_output_n;
unspent.amount = unspent.value;
let chunksIn = bitcoin.script.decompile(Buffer.from(unspent.script, 'hex'));
unspent.address = bitcoin.address.fromOutputScript(chunksIn);
utxos.push(unspent);
}
} catch (err) {
console.warn(err);
}
this.utxo = utxos;
}
weOwnAddress(addr) {
let hashmap = {};
for (let a of this.usedAddresses) {
hashmap[a] = 1;
}
return hashmap[addr] === 1;
}
2018-07-21 14:52:54 +02:00
}