BlueWallet/class/wallets/legacy-wallet.js

376 lines
11 KiB
JavaScript
Raw Normal View History

import { randomBytes } from '../rng';
2018-03-24 22:24:20 +01:00
import { AbstractWallet } from './abstract-wallet';
import { HDSegwitBech32Wallet } from '..';
2019-09-14 00:15:59 +02:00
const bitcoin = require('bitcoinjs-lib');
const BlueElectrum = require('../../BlueElectrum');
const coinSelectAccumulative = require('coinselect/accumulative');
const coinSelectSplit = require('coinselect/split');
2018-03-20 21:41:07 +01:00
/**
* Has private key and single address like "1ABCD....."
2018-03-20 21:41:07 +01:00
* (legacy P2PKH compressed)
*/
export class LegacyWallet extends AbstractWallet {
static type = 'legacy';
static typeReadable = 'Legacy (P2PKH)';
2018-03-20 21:41:07 +01:00
2018-07-14 22:32:36 +02:00
/**
* Simple function which says that we havent tried to fetch balance
* for a long time
*
* @return {boolean}
*/
timeToRefreshBalance() {
2018-07-14 21:52:27 +02:00
if (+new Date() - this._lastBalanceFetch >= 5 * 60 * 1000) {
2018-07-02 15:51:24 +02:00
return true;
}
2018-07-14 22:32:36 +02:00
return false;
}
2018-07-12 01:38:24 +02:00
2018-07-14 22:32:36 +02:00
/**
* Simple function which says if we hve some low-confirmed transactions
* and we better fetch them
*
* @return {boolean}
*/
timeToRefreshTransaction() {
for (const tx of this.getTransactions()) {
2018-07-12 01:38:24 +02:00
if (tx.confirmations < 7) {
return true;
}
}
2018-07-14 22:32:36 +02:00
return false;
2018-07-02 15:51:24 +02:00
}
async generate() {
const buf = await randomBytes(32);
2020-05-01 02:36:52 +02:00
this.secret = bitcoin.ECPair.makeRandom({ rng: () => buf }).toWIF();
2018-03-20 21:41:07 +01:00
}
/**
*
* @returns {string}
*/
getAddress() {
if (this._address) return this._address;
let address;
try {
const keyPair = bitcoin.ECPair.fromWIF(this.secret);
2019-09-14 00:15:59 +02:00
address = bitcoin.payments.p2pkh({
pubkey: keyPair.publicKey,
}).address;
2018-03-20 21:41:07 +01:00
} catch (err) {
return false;
}
this._address = address;
return this._address;
}
/**
2018-06-17 12:46:19 +02:00
* Fetches balance of the Wallet via API.
* Returns VOID. Get the actual balance via getter.
*
* @returns {Promise.<void>}
*/
2018-03-20 21:41:07 +01:00
async fetchBalance() {
try {
const balance = await BlueElectrum.getBalanceByAddress(this.getAddress());
this.balance = Number(balance.confirmed);
this.unconfirmed_balance = Number(balance.unconfirmed);
2018-07-02 11:48:40 +02:00
this._lastBalanceFetch = +new Date();
} catch (Error) {
console.warn(Error);
2018-03-20 21:41:07 +01:00
}
}
2018-06-17 12:46:19 +02:00
/**
* Fetches UTXO from API. Returns VOID.
*
* @return {Promise.<void>}
*/
2018-03-20 21:41:07 +01:00
async fetchUtxo() {
try {
const utxos = await BlueElectrum.multiGetUtxoByAddress([this.getAddress()]);
this.utxo = [];
for (const arr of Object.values(utxos)) {
this.utxo = this.utxo.concat(arr);
}
// now we need to fetch txhash for each input as required by PSBT
if (LegacyWallet.type !== this.type) return; // but only for LEGACY single-address wallets
const txhexes = await BlueElectrum.multiGetTransactionByTxid(
this.utxo.map(u => u.txId),
50,
false,
);
const newUtxos = [];
for (const u of this.utxo) {
if (txhexes[u.txId]) u.txhex = txhexes[u.txId];
newUtxos.push(u);
}
this.utxo = newUtxos;
} catch (Error) {
console.warn(Error);
}
2018-03-20 21:41:07 +01:00
}
getUtxo() {
const ret = [];
for (const u of this.utxo) {
if (u.txId) u.txid = u.txId;
if (!u.confirmations && u.height) u.confirmations = BlueElectrum.estimateCurrentBlockheight() - u.height;
ret.push(u);
}
return ret;
}
2018-06-17 12:46:19 +02:00
/**
* Fetches transactions via Electrum. Returns VOID.
* Use getter to get the actual list. *
* @see AbstractHDElectrumWallet.fetchTransactions()
2018-06-17 12:46:19 +02:00
*
* @return {Promise.<void>}
*/
2018-03-20 21:41:07 +01:00
async fetchTransactions() {
// Below is a simplified copypaste from HD electrum wallet
this._txs_by_external_index = [];
const addresses2fetch = [this.getAddress()];
// first: batch fetch for all addresses histories
const histories = await BlueElectrum.multiGetHistoryByAddress(addresses2fetch);
const txs = {};
for (const history of Object.values(histories)) {
for (const tx of history) {
txs[tx.tx_hash] = tx;
2018-03-20 21:41:07 +01:00
}
}
2018-03-20 21:41:07 +01:00
// next, batch fetching each txid we got
const txdatas = await BlueElectrum.multiGetTransactionByTxid(Object.keys(txs));
2018-07-25 01:27:21 +02:00
// 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)
const vinTxids = [];
for (const txdata of Object.values(txdatas)) {
for (const vin of txdata.vin) {
vinTxids.push(vin.txid);
}
}
const vintxdatas = await BlueElectrum.multiGetTransactionByTxid(vinTxids);
// fetched all transactions from our inputs. now we need to combine it.
// iterating all _our_ transactions:
for (const txid of Object.keys(txdatas)) {
// iterating all inputs our our single transaction:
for (let inpNum = 0; inpNum < txdatas[txid].vin.length; inpNum++) {
const inpTxid = txdatas[txid].vin[inpNum].txid;
const 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;
2018-03-20 21:41:07 +01:00
}
}
}
// now, we need to put transactions in all relevant `cells` of internal hashmaps: this.transactions_by_internal_index && this.transactions_by_external_index
for (const tx of Object.values(txdatas)) {
for (const vin of tx.vin) {
if (vin.addresses && vin.addresses.indexOf(this.getAddress()) !== -1) {
// this TX is related to our address
const clonedTx = Object.assign({}, tx);
clonedTx.inputs = tx.vin.slice(0);
clonedTx.outputs = tx.vout.slice(0);
delete clonedTx.vin;
delete clonedTx.vout;
this._txs_by_external_index.push(clonedTx);
}
}
for (const vout of tx.vout) {
if (vout.scriptPubKey.addresses && vout.scriptPubKey.addresses.indexOf(this.getAddress()) !== -1) {
// this TX is related to our address
const clonedTx = Object.assign({}, tx);
clonedTx.inputs = tx.vin.slice(0);
clonedTx.outputs = tx.vout.slice(0);
delete clonedTx.vin;
delete clonedTx.vout;
this._txs_by_external_index.push(clonedTx);
}
}
}
this._lastTxFetch = +new Date();
}
getTransactions() {
// a hacky code reuse from electrum HD wallet:
this._txs_by_external_index = this._txs_by_external_index || [];
this._txs_by_internal_index = [];
const hd = new HDSegwitBech32Wallet();
return hd.getTransactions.apply(this);
}
2020-04-28 18:27:35 +02:00
/**
* Broadcast txhex. Can throw an exception if failed
*
* @param {String} txhex
* @returns {Promise<boolean>}
*/
2018-03-20 21:41:07 +01:00
async broadcastTx(txhex) {
const broadcast = await BlueElectrum.broadcastV2(txhex);
2020-04-28 18:27:35 +02:00
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
}
/**
*
* @param utxos {Array.<{vout: Number, value: Number, txId: String, address: String, txhex: 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
* @param masterFingerprint {number} Decimal number of wallet's master fingerprint
* @returns {{outputs: Array, tx: Transaction, inputs: Array, fee: Number, psbt: Psbt}}
*/
createTransaction(utxos, targets, feeRate, changeAddress, sequence, skipSigning = false, masterFingerprint) {
if (!changeAddress) throw new Error('No change address provided');
sequence = sequence || 0xffffffff; // disable RBF by default
let algo = coinSelectAccumulative;
if (targets.length === 1 && targets[0] && !targets[0].value) {
// we want to send MAX
algo = coinSelectSplit;
}
const { 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');
}
const psbt = new bitcoin.Psbt();
let c = 0;
const values = {};
let keyPair;
inputs.forEach(input => {
if (!skipSigning) {
// skiping signing related stuff
keyPair = bitcoin.ECPair.fromWIF(this.secret); // secret is WIF
}
values[c] = input.value;
c++;
if (!input.txhex) throw new Error('UTXO is missing txhex of the input, which is required by PSBT for non-segwit input');
psbt.addInput({
hash: input.txid,
index: input.vout,
sequence,
// non-segwit inputs now require passing the whole previous tx as Buffer
nonWitnessUtxo: Buffer.from(input.txhex, 'hex'),
});
});
outputs.forEach(output => {
// if output has no address - this is change output
if (!output.address) {
output.address = changeAddress;
}
const outputData = {
address: output.address,
value: output.value,
};
psbt.addOutput(outputData);
});
if (!skipSigning) {
// skiping signing related stuff
for (let cc = 0; cc < c; cc++) {
psbt.signInput(cc, keyPair);
}
}
let tx;
if (!skipSigning) {
tx = psbt.finalizeAllInputs().extractTransaction();
}
return { tx, inputs, outputs, fee, psbt };
}
2018-06-17 12:46:44 +02:00
2018-06-25 00:22:46 +02:00
getLatestTransactionTime() {
if (this.getTransactions().length === 0) {
return 0;
}
let max = 0;
for (const tx of this.getTransactions()) {
max = Math.max(new Date(tx.received) * 1, max);
2018-06-25 00:22:46 +02:00
}
return new Date(max).toString();
2018-06-25 00:22:46 +02:00
}
/**
* Validates any address, including legacy, p2sh and bech32
*
* @param address
* @returns {boolean}
*/
2018-07-12 01:38:24 +02:00
isAddressValid(address) {
try {
bitcoin.address.toOutputScript(address);
return true;
} catch (e) {
return false;
}
}
/**
* Converts script pub key to legacy address if it can. Returns FALSE if it cant.
*
* @param scriptPubKey
* @returns {boolean|string} Either p2pkh address or false
*/
static scriptPubKeyToAddress(scriptPubKey) {
const scriptPubKey2 = Buffer.from(scriptPubKey, 'hex');
let ret;
try {
ret = bitcoin.payments.p2pkh({
output: scriptPubKey2,
network: bitcoin.networks.bitcoin,
}).address;
} catch (_) {
return false;
}
return ret;
}
weOwnAddress(address) {
return this.getAddress() === address || this._address === address;
}
allowSendMax() {
return true;
}
async getChangeAddressAsync() {
return new Promise(resolve => {
resolve(this.getAddress());
});
}
2018-03-20 21:41:07 +01:00
}