Untitled
unknown
typescript
6 months ago
8.8 kB
2
Indexable
Never
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';