import { Logger } from '@nestjs/common';
import { NetworkManagerAbstract } from '@emerald-wallet/networks-core';
import { BlockbookBitcoin } from 'blockbook-client';
import HDKey from 'hdkey';
import {
WalletNetworkRequest,
WalletNetworkBase,
IndexerLastBlock,
TransactionNetworkResponse,
TransactionSendNetworkRequest,
TransactionEstimateFeeResponse,
TransactionSendNetworkResponse,
TransactionTypeNetwork,
TransactionStatus,
NetworkError,
TransactionSubscribeCallback
} from '@emerald-wallet/types';
import { BitcoinNetworkManagerClient } from './client';
import { AddressType, DerivationPath, Network, NetworkName, UTXO } from './types';
import { BlockInfoBitcoin, NormalizedTxCommon, UtxoDetails } from 'blockbook-client/dist/lib/types';
import bitcoin from './bitcoin';
import _ from 'lodash';
export interface BitcoinNetworkManagerOptions {
blockbooks: string[],
network: NetworkName,
testnet?: boolean
}
export class BitcoinNetworkManager extends BitcoinNetworkManagerClient implements NetworkManagerAbstract {
private networkName: NetworkName;
private blockbook: BlockbookBitcoin;
constructor (options: BitcoinNetworkManagerOptions) {
super(options.network, options.testnet);
this.networkName = options.network;
this.blockbook = new BlockbookBitcoin({nodes: options.blockbooks});
}
private getKeypair (seed: string, index: number): HDKey {
const network = this.getNetwork();
return HDKey.fromMasterSeed(Buffer.from(seed, 'hex'), network.bip32)
.derive(this.getDerivationPath(index));
}
private getAddressType (network: Network) {
return network.bech32 ? AddressType.p2wpkh : AddressType.p2pkh;
}
private getAddress (keypair: HDKey): string {
const network = this.getNetwork() as any;
const inputType = this.getAddressType(network);
const payment = bitcoin.payments[inputType]({pubkey: keypair.publicKey, network});
return payment.address!;
}
private transformTransaction (tx: NormalizedTxCommon): TransactionNetworkResponse[] | undefined {
if (!tx.vin.length || !tx.vout.length)
return undefined;
//const amount = Number(tx.vin[0].value) / SATOSHIS_PER_BITCOIN
const address_from = tx.vin.find(input => input.addresses && input.addresses.length && input.value)?.addresses?.[0];
const outputs = tx.vout.filter(output => output.addresses && output.addresses.length && output.value)
.map(output => ({
address_to: output.addresses![0],
amount: Number(output.value),
}));
const outputsTransformed = outputs.map(output => ({
txid: tx.txid,
type: TransactionTypeNetwork.TRANSFER,
time: new Date(tx.blockTime * 1000),
status: TransactionStatus.WAITING,
blockHeight: tx.blockHeight,
fee: Number(tx.fees),
amount: output.amount,
address_from,
address_to: output.address_to
}));
return Object.values(outputsTransformed.reduce((acc, tx) => {
if (tx.address_from != tx.address_to || outputsTransformed.length == 1)
acc[tx.address_to!] = {
...tx,
amount: (acc[tx.address_to!] ? acc[tx.address_to!].amount : 0) + tx.amount
};
return acc;
}, {}));
}
private async getUtxos (address: string): Promise<UTXO[]> {
//let utxosAccumulate: UtxoDetails[] = []
//const network = this.getNetwork() as any
//const inputType = this.getInputType(network)
const utxos = _.orderBy(await this.blockbook.getUtxosForAddress(address), 'value', 'desc');
// for (const utxo of utxos) {
// utxosAccumulate.push(utxo)
// amount += bitcoin.Transaction.IN_SIZES[inputType] * estimateFee
// if (_.sumBy(utxosAccumulate, "value") > amount)
// break;
// }
return Promise.all(utxos.map(async utxo => ({
hash: utxo.txid,
index: utxo.vout,
value: Number(utxo.value),
nonWitnessUtxo: Buffer.from((await this.blockbook.getTx(utxo.txid)).hex!, 'hex')
})));
}
private createPsbt (payload: TransactionSendNetworkRequest, utxos: UTXO[], chargeAddress: string, fee?: number): bitcoin.Psbt {
const network = this.getNetwork() as any;
const value = payload.amount;
let psbt = new bitcoin.Psbt({network, maximumFeeRate: 7500})
.addInputs(utxos)
.addOutput({address: payload.address, value});
if (payload.provider_address && payload.provider_fee) {
psbt.addOutput({
address: payload.provider_address,
value: payload.provider_fee
});
}
if (fee) {
let charge = _.sumBy(utxos, 'value') - (fee + value);
if (payload.provider_address && payload.provider_fee)
charge -= payload.provider_fee;
// Money back
if (charge > 0)
psbt.addOutput({address: chargeAddress, value: Math.ceil(charge)});
}
return psbt;
}
private async calculateFees (psbt: bitcoin.Psbt, keypair: HDKey): Promise<number> {
const network = this.getNetwork();
let estimateFee = (Number(await this.blockbook.estimateFee(3)) * 100000) || 1;
if (estimateFee < 0) estimateFee = 1;
psbt.signAllInputs(keypair);
psbt.finalizeAllInputs();
const addressType = this.getAddressType(network);
if (!addressType)
throw new Error(NetworkError.ADDRESS_NOT_VALID);
const tx = psbt.extractTransaction(true);
const vBytes = tx.virtualSize() + bitcoin.Transaction.OUT_SIZES[addressType as AddressType] + 10;
return Math.ceil(vBytes * estimateFee);
}
private async signTransaction (payload: TransactionSendNetworkRequest): Promise<string> {
const keypair = this.getKeypair(payload.seed, payload.wallet_index);
const address = this.getAddress(keypair);
const utxos = await this.getUtxos(address);
const psbt = this.createPsbt(payload, utxos, address);
const fees = await this.calculateFees(psbt, keypair);
const psbtWithNormalFees = this.createPsbt(payload, utxos, address, fees);
psbtWithNormalFees.signAllInputs(keypair);
psbtWithNormalFees.finalizeAllInputs();
return psbtWithNormalFees.extractTransaction().toHex();
}
getDerivationPath (wallet_index: number): string {
return DerivationPath[this.networkName].replace('account', String(wallet_index));
}
getWalletAddress (data: WalletNetworkRequest): string {
const keypair = this.getKeypair(data.seed, data.index);
return this.getAddress(keypair);
}
async getWallet (data: WalletNetworkRequest): Promise<WalletNetworkBase> {
const keypair = this.getKeypair(data.seed, data.index);
const address = this.getAddress(keypair);
const account = await this.blockbook.getAddressDetails(address, {details: 'basic'});
return {
address,
balance: Number(account.balance)
};
}
async getLastBlockHeight (last_processed_block_height?: number | undefined): Promise<IndexerLastBlock> {
const status = await this.blockbook.getStatus();
return {
height: status.blockbook.bestHeight
};
}
async getBlockTransactions (height: number): Promise<TransactionNetworkResponse[]> {
const txs: BlockInfoBitcoin['txs'] = [];
let block = await this.blockbook.getBlock(height).catch(e => {
if (e.message == 'Not Found') return [];
throw new Error(e);
});
if (!('txs' in block)) return [];
txs.push(...block.txs!);
while (block.page < block.totalPages) {
block = await this.blockbook.getBlock(height, {page: block.page});
txs.push(...block.txs!);
block.page++;
}
return txs
.map(tx => this.transformTransaction(tx))
.filter(tx => !!tx)
.flat() as TransactionNetworkResponse[];
}
async estimateFee (data: TransactionSendNetworkRequest): Promise<TransactionEstimateFeeResponse> {
data.amount = Math.floor(data.amount);
const keypair = this.getKeypair(data.seed, data.wallet_index);
const address = this.getAddress(keypair);
const utxos = await this.getUtxos(address);
const psbt = this.createPsbt(data, utxos, address);
const fees = await this.calculateFees(psbt, keypair);
return {
fee: fees + (data.provider_fee || 0)
};
}
async sendTransaction (data: TransactionSendNetworkRequest): Promise<TransactionSendNetworkResponse> {
data.amount = Math.floor(data.amount);
const txHex = await this.signTransaction(data);
const txid = await this.blockbook.sendTx(txHex);
return {txid};
}
async subscribeTransactions (callback: TransactionSubscribeCallback) {
const _this = this;
await this.blockbook.subscribe('subscribeNewTransaction', {}, async function (data) {
const transactions = _this.transformTransaction(data);
if (transactions)
transactions.map(tx => callback(tx));
});
}
async connect () {
await this.blockbook.connect();
}
disconnect () {
this.blockbook.disconnect();
}
}
export * from './types';