Untitled

 avatar
unknown
typescript
2 years ago
8.8 kB
3
Indexable
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';