UNPKG

hodl-wallet

Version:

🧊 HODL Wallet - Fast CLI crypto wallet!

226 lines (188 loc) 7.48 kB
import BaseNetwork from './BaseNetwork.js'; import * as bitcoin from 'bitcoinjs-lib'; import bip39 from 'bip39'; import * as ecc from 'tiny-secp256k1'; import { BIP32Factory } from 'bip32'; import { ECPairFactory } from 'ecpair'; import axios from 'axios'; const bip32 = BIP32Factory(ecc); const ECPair = ECPairFactory(ecc); export default class BitcoinNetwork extends BaseNetwork { constructor(config) { super(); this.config = config; this.network = bitcoin.networks[config.network || 'bitcoin']; this.url = config.url; } async getBalance(address) { try { const response = await axios.get(`${this.url}/address/${address}`); const chainStats = response.data.chain_stats; const mempoolStats = response.data.mempool_stats; const balance = (chainStats.funded_txo_sum - chainStats.spent_txo_sum) + (mempoolStats.funded_txo_sum - mempoolStats.spent_txo_sum); return this.satoshisToBTC(balance); } catch (error) { throw new Error(`Failed to get balance: ${error.message}`); } } async getTokenBalances(address) { const balances = []; const nativeBalance = await this.getBalance(address); balances.push([this.config.nativeToken, nativeBalance]); return balances; } async transfer(from, to, amount, options = {}) { try { const utxos = await this.getUTXOs(from.address); const satoshis = BigInt(this.BTCToSatoshis(amount)); const feeRate = options.feeRate || 10; // sats/byte // Create PSBT instance with network const psbt = new bitcoin.Psbt({ network: this.network }); psbt.setVersion(2); psbt.setLocktime(0); let totalInputValue = 0n; // Add inputs for (const utxo of utxos) { // Get the full transaction for nonWitnessUtxo const txHex = await this.getTransaction(utxo.txid); psbt.addInput({ hash: utxo.txid, index: utxo.vout, nonWitnessUtxo: Buffer.from(txHex, 'hex'), sequence: 0xffffffff }); totalInputValue += BigInt(utxo.value); // Break if we have enough funds (considering estimated fee) const estimatedFee = BigInt(this.estimateTxSize(psbt.inputCount, 2) * feeRate); if (totalInputValue >= satoshis + estimatedFee) { break; } } if (totalInputValue < satoshis + BigInt(feeRate)) { throw new Error('Insufficient balance for the transaction including fees.'); } // Add recipient output psbt.addOutput({ address: to, value: satoshis }); // Add change output if needed const estimatedFee = BigInt(this.estimateTxSize(psbt.inputCount, 2) * feeRate); const changeValue = totalInputValue - satoshis - estimatedFee; if (changeValue > 546n) { // Dust threshold psbt.addOutput({ address: from.address, value: changeValue }); } // Sign all inputs const keyPair = ECPair.fromWIF(from.privateKey, this.network); for (let i = 0; i < psbt.inputCount; i++) { psbt.signInput(i, keyPair); // Validate signature const valid = psbt.validateSignaturesOfInput(i, (pubkey, msghash, signature) => { return ECPair.fromPublicKey(pubkey).verify(msghash, signature); }); if (!valid) { throw new Error(`Signature validation failed for input ${i}`); } } // Finalize and extract transaction psbt.finalizeAllInputs(); return psbt.extractTransaction().toHex(); } catch (error) { throw new Error(`Failed to create transaction: ${error.message}`); } } // Add this helper method to get full transaction data async getTransaction(txid) { try { const response = await axios.get(`${this.url}/tx/${txid}/hex`); return response.data; } catch (error) { throw new Error(`Failed to get transaction: ${error.message}`); } } // Método para estimar el tamaño de la transacción estimateTxSize(numInputs, numOutputs) { return numInputs * 68 + numOutputs * 31 + 10; // Aproximación para P2WPKH } // Helper function to create account details from a key pair createAccountDetails(keyPair) { const { address } = bitcoin.payments.p2wpkh({ pubkey: keyPair.publicKey, network: this.network }); return { address, privateKey: keyPair.toWIF(), publicKey: keyPair.publicKey.toString('hex') }; } async createAccount() { const keyPair = ECPair.makeRandom({ network: this.network }); return this.createAccountDetails(keyPair); } privateKeyToAccount(privateKeyWIF) { try { const keyPair = ECPair.fromWIF(privateKeyWIF, this.network); return this.createAccountDetails(keyPair); } catch (error) { throw new Error(`Failed to derive account from private key: ${error.message}`); } } async accountFromMnemonic(mnemonic) { const seed = await bip39.mnemonicToSeed(mnemonic); const root = bip32.fromSeed(seed, this.network); const path = `m/84'/0'/0'/0/0`; // BIP84 para SegWit nativo const child = root.derivePath(path); const { address } = bitcoin.payments.p2wpkh({ pubkey: child.publicKey, network: this.network }); return { address, privateKey: child.toWIF(), publicKey: child.publicKey.toString('hex'), mnemonic }; } async createAccountFromMnemonic() { try { const mnemonic = bip39.generateMnemonic(); return this.accountFromMnemonic(mnemonic); } catch (error) { throw new Error('Failed to create account from mnemonic: ' + error.message); } } validateMnemonic(mnemonic) { return bip39.validateMnemonic(mnemonic); } // Métodos auxiliares satoshisToBTC(satoshis) { return satoshis / 100000000; } BTCToSatoshis(btc) { return Math.floor(btc * 100000000); } async getUTXOs(address) { try { const response = await axios.get(`${this.url}/address/${address}/utxo`); return response.data; } catch (error) { throw new Error(`Failed to get UTXOs: ${error.message}`); } } async handleNativeTransfer(from, to, amount, options = {}) { return this.transfer(from, to, amount, options); } async sendSignedTransaction(signedTx) { try { const response = await axios.post(`${this.url}/tx`, signedTx); return { transactionHash: response.data }; } catch (error) { throw new Error(`Failed to broadcast transaction: ${error.message}`); } } }