You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

299 lines
8.7 KiB

7 years ago
import { AbstractWallet } from './abstract-wallet';
import { HDSegwitBech32Wallet } from './';
import { NativeModules } from 'react-native';
const bitcoin = require('bitcoinjs-lib');
const { RNRandomBytes } = NativeModules;
7 years ago
const BigNumber = require('bignumber.js');
const signer = require('../models/signer');
const BlueElectrum = require('../BlueElectrum');
/**
* Has private key and single address like "1ABCD....."
* (legacy P2PKH compressed)
*/
export class LegacyWallet extends AbstractWallet {
static type = 'legacy';
static typeReadable = 'Legacy (P2PKH)';
/**
* Simple function which says that we havent tried to fetch balance
* for a long time
*
* @return {boolean}
*/
timeToRefreshBalance() {
6 years ago
if (+new Date() - this._lastBalanceFetch >= 5 * 60 * 1000) {
return true;
}
return false;
}
/**
* Simple function which says if we hve some low-confirmed transactions
* and we better fetch them
*
* @return {boolean}
*/
timeToRefreshTransaction() {
for (let tx of this.getTransactions()) {
if (tx.confirmations < 7) {
return true;
}
}
return false;
}
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 = bitcoin.ECPair.makeRandom({
rng: function(length) {
return buf;
},
}).toWIF();
resolve();
});
}
// RN environment
RNRandomBytes.randomBytes(32, (err, bytes) => {
if (err) throw new Error(err);
that.secret = bitcoin.ECPair.makeRandom({
rng: function(length) {
let b = Buffer.from(bytes, 'base64');
return b;
},
}).toWIF();
resolve();
});
});
}
/**
*
* @returns {string}
*/
getAddress() {
if (this._address) return this._address;
let address;
try {
let keyPair = bitcoin.ECPair.fromWIF(this.secret);
address = bitcoin.payments.p2pkh({
pubkey: keyPair.publicKey,
}).address;
} catch (err) {
return false;
}
this._address = address;
return this._address;
}
/**
6 years ago
* Fetches balance of the Wallet via API.
* Returns VOID. Get the actual balance via getter.
*
* @returns {Promise.<void>}
*/
async fetchBalance() {
try {
let balance = await BlueElectrum.getBalanceByAddress(this.getAddress());
this.balance = Number(balance.confirmed);
this.unconfirmed_balance = new BigNumber(balance.unconfirmed);
this.unconfirmed_balance = this.unconfirmed_balance.dividedBy(100000000).toString() * 1; // wtf
6 years ago
this._lastBalanceFetch = +new Date();
} catch (Error) {
console.warn(Error);
}
}
6 years ago
/**
* Fetches UTXO from API. Returns VOID.
*
* @return {Promise.<void>}
*/
async fetchUtxo() {
try {
let utxos = await BlueElectrum.multiGetUtxoByAddress([this.getAddress()]);
for (let arr of Object.values(utxos)) {
this.utxo = this.utxo.concat(arr);
}
} catch (Error) {
console.warn(Error);
}
// backward compatibility
for (let u of this.utxo) {
u.tx_output_n = u.vout;
u.tx_hash = u.txId;
u.confirmations = u.height ? 1 : 0;
}
}
getUtxo() {
return this.utxo;
}
6 years ago
/**
* Fetches transactions via Electrum. Returns VOID.
* Use getter to get the actual list. *
* @see AbstractHDElectrumWallet.fetchTransactions()
6 years ago
*
* @return {Promise.<void>}
*/
async fetchTransactions() {
// Below is a simplified copypaste from HD electrum wallet
this._txs_by_external_index = [];
let addresses2fetch = [this.getAddress()];
// 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, we need to put transactions in all relevant `cells` of internal hashmaps: this.transactions_by_internal_index && this.transactions_by_external_index
for (let tx of Object.values(txdatas)) {
for (let vin of tx.vin) {
if (vin.addresses && vin.addresses.indexOf(this.getAddress()) !== -1) {
// this TX is related to our address
let 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 (let vout of tx.vout) {
if (vout.scriptPubKey.addresses.indexOf(this.getAddress()) !== -1) {
// this TX is related to our address
let 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 = [];
let hd = new HDSegwitBech32Wallet();
return hd.getTransactions.apply(this);
}
async broadcastTx(txhex) {
try {
const broadcast = await BlueElectrum.broadcast(txhex);
return broadcast;
} catch (error) {
return error;
}
}
/**
* Takes UTXOs, transforms them into
* format expected by signer module, creates tx and returns signed string txhex.
*
* @param utxos Unspent outputs, expects blockcypher format
* @param amount
* @param fee
* @param toAddress
* @param memo
* @return string Signed txhex ready for broadcast
*/
createTx(utxos, amount, fee, toAddress, memo) {
// transforming UTXOs fields to how module expects it
for (let u of utxos) {
u.confirmations = 6; // hack to make module accept 0 confirmations
u.txid = u.tx_hash;
u.vout = u.tx_output_n;
u.amount = new BigNumber(u.value);
u.amount = u.amount.dividedBy(100000000);
u.amount = u.amount.toString(10);
}
// console.log('creating legacy tx ', amount, ' with fee ', fee, 'secret=', this.getSecret(), 'from address', this.getAddress());
let amountPlusFee = parseFloat(new BigNumber(amount).plus(fee).toString(10));
return signer.createTransaction(utxos, toAddress, amountPlusFee, fee, this.getSecret(), this.getAddress());
}
6 years ago
6 years ago
getLatestTransactionTime() {
if (this.getTransactions().length === 0) {
return 0;
}
let max = 0;
for (let tx of this.getTransactions()) {
max = Math.max(new Date(tx.received) * 1, max);
6 years ago
}
return new Date(max).toString();
6 years ago
}
/**
* Validates any address, including legacy, p2sh and bech32
*
* @param address
* @returns {boolean}
*/
isAddressValid(address) {
try {
bitcoin.address.toOutputScript(address);
return true;
} catch (e) {
return false;
}
}
weOwnAddress(address) {
return this.getAddress() === address || this._address === address;
}
}