UNPKG

minimal-slp-wallet

Version:
375 lines (304 loc) 12.2 kB
/* This library contains functions specific to sending BCH. */ // let this // const BCHJS = require('@psf/bch-js') const bchDonation = require('bch-donation') // const AdapterRouter = require('./adapters/router') // Send the Permissionless Software Foundation a donation to thank them for creating // and maintaining this software. const PSF_DONATION = 2000 class SendBCH { constructor (localConfig = {}) { // Dependency injection. this.bchjs = localConfig.bchjs if (!this.bchjs) { throw new Error( 'Must pass instance of bch-js when instantiating SendBCH.' ) } this.ar = localConfig.ar if (!this.ar) { throw new Error('Must pass instance of Adapter Router.') } this.restURL = localConfig.restURL this.apiToken = localConfig.apiToken // this.ar = new AdapterRouter({ bchjs: this.bchjs }) // this.bchjs = new BCHJS(config) // This should be the last command in the constructor. // this is a local global variable for when 'this' loses scope. // this = this } // Top-level function that orchestrates the sending of BCH. // Expects an array of BCH-only UTXOs. Does not check if UTXOs belong to a token, // and will burn tokens if they are included in the UTXO list. async sendBch (outputs, walletInfo, utxos) { // console.log('sendBch() walletInfo: ', walletInfo) try { // Generate the transaction. const transaction = await this.createTransaction( outputs, walletInfo, utxos ) // console.log('transaction hex: ', transaction.hex) // Broadcast the transaction to the network. // const txid = await this.bchjs.RawTransactions.sendRawTransaction( // transaction.hex // ) const txid = await this.ar.sendTx(transaction.hex) // console.log('sendBch(): ', txid) // TODO: Remove the spent UTXOs from the utxoStore. return txid } catch (err) { console.error('Error in send-bch.js/sendBch()') throw err } } // outputs is an array of output objects. Look like this: // { // address: "bitcoincash:qp2rmj8heytjrksxm2xrjs0hncnvl08xwgkweawu9h", // // amount in satoshis, 1 satoshi = 0.00000001 Bitcoin // amountSat: 100000 // } // Expects an array of BCH-only UTXOs. Does not check if UTXOs belong to a token, // and will burn tokens if they are included in the UTXO list. async createTransaction (outputs, walletInfo, utxos) { // console.log('createTransaction() walletInfo: ', walletInfo) try { // If the BCH utxos array is still empty, then throw an error. if (!utxos || utxos.length === 0) { throw new Error('UTXO list is empty') } // Default value of 1 sat-per-byte if mining fee is not specified. if (!walletInfo.fee) walletInfo.fee = 1.0 // Determine the UTXOs needed to be spent for this TX, and the change // that will be returned to the wallet. const { necessaryUtxos, change } = this.getNecessaryUtxosAndChange( outputs, utxos, walletInfo.fee ) // Create an instance of the Transaction Builder. const transactionBuilder = new this.bchjs.TransactionBuilder() // Add inputs necessaryUtxos.forEach(utxo => { transactionBuilder.addInput(utxo.tx_hash, utxo.tx_pos) }) // Add outputs outputs.forEach(receiver => { transactionBuilder.addOutput( this.bchjs.Address.toLegacyAddress(receiver.address), receiver.amountSat) }) // Send a 2000 sat donation to PSF to thank them for creating this awesome software. // console.log(`psf: ${bchDonation('psf').donations}`) transactionBuilder.addOutput(bchDonation('psf').donations, PSF_DONATION) // Send change back to the wallet, if it's bigger than dust. // console.log(`change: ${change}`) if (change && change > 546) { transactionBuilder.addOutput(walletInfo.cashAddress, change) } // Generate a key pair from the mnemonic. const keyPair = await this.getKeyPairFromMnemonic(walletInfo) // Sign each UTXO that is about to be spent. necessaryUtxos.forEach((utxo, i) => { let redeemScript transactionBuilder.sign( i, keyPair, redeemScript, transactionBuilder.hashTypes.SIGHASH_ALL, utxo.value ) }) // Build the transaction, return the compiled transaction in hex format. const tx = transactionBuilder.build() return { hex: tx.toHex(), txid: tx.getId() } } catch (err) { console.error('Error in send-bch.js/createTransaction()') throw err } } // Get the UTXOs required to generate a transaction. // Uses the smallest UTXOs first, which maximizes the number UTXOs used. // This helps reduce the total number UTXOs in the wallet, which is efficient // for limiting the number of network calls, and leads to better UX. getNecessaryUtxosAndChange (outputs, availableUtxos, satsPerByte = 1.0, opts = {}) { const sortedUtxos = (opts.utxoSortingFn ? opts.utxoSortingFn(availableUtxos) : this.sortUtxosBySize(availableUtxos, 'ASCENDING') ) // console.log(`sortedUtxos: ${JSON.stringify(sortedUtxos, null, 2)}`) // const satsPerByte = 1.0 // Calculate the miner fee, assuming inputs (this cost will be added later). // Change is assumed, that is the +1. const fee = this.calculateFee(0, outputs.length + 1, satsPerByte) // console.log(`fee: ${fee}`) // Calculate the satoshis needed (minus the fee for each input) const satoshisToSend = outputs.reduce( (acc, receiver) => acc + receiver.amountSat, 0 ) let satoshisNeeded = satoshisToSend + fee + PSF_DONATION let satoshisAvailable = 0 const necessaryUtxos = [] // Add each UTXO to the calculation until enough satoshis are found. for (const utxo of sortedUtxos) { // TODO: Check getTxOut() on the full node to verify the UTXO is valid. // TODO: Check the ancestor history to make sure the UTXO won't fail the // 50-chain limit rule. // Add the next UTXO. necessaryUtxos.push(utxo) satoshisAvailable += utxo.value // Additional cost per Utxo input is 148 sats for mining fees. satoshisNeeded += 148 // Exit the loop once enough UTXOs are found to pay the the TX. if (satoshisAvailable >= satoshisNeeded) { break } } // Calculate the remainder or 'change' to send back to the sender. const change = satoshisAvailable - satoshisNeeded // console.log(`change: ${change}`) // If change is less than zero, something went wrong. Sanity check. if (change < 0) { console.error( `Available satoshis (${satoshisAvailable}) below needed satoshis (${satoshisNeeded}).` ) throw new Error('Insufficient balance') } // console.log(`necessaryUtxos: ${JSON.stringify(necessaryUtxos, null, 2)}`) // console.log(`change: ${JSON.stringify(change, null, 2)}`) return { necessaryUtxos, change } } // Sort the UTXOs by the size of satoshis they hold. sortUtxosBySize (utxos, sortingOrder = 'ASCENDING') { if (sortingOrder === 'ASCENDING') { return utxos.sort((a, b) => a.value - b.value) } else { return utxos.sort((a, b) => b.value - a.value) } } // Calculate the miner fee that needs to be paid for this transaction. calculateFee (numInputs, numOutputs, satsPerByte) { try { const byteCount = this.bchjs.BitcoinCash.getByteCount( { P2PKH: numInputs }, { P2PKH: numOutputs + 1 } ) const fee = Math.ceil(byteCount * satsPerByte) if (isNaN(fee)) { throw new Error('Invalid input. Fee could not be calculated.') } return fee } catch (err) { console.error('Error in send-bch.js/calculateFee()') throw err } } // TODO: change the name of this function to getKeyPair() // Generate a EC key pair. async getKeyPairFromMnemonic (walletInfo) { // console.log('getKeyPairFromMnemonic() walletInfo: ', walletInfo) // If the wallet has a 12-word mnemonic, generate the key from there. if (walletInfo.mnemonic) { const rootSeed = await this.bchjs.Mnemonic.toSeed(walletInfo.mnemonic) const masterHDNode = this.bchjs.HDNode.fromSeed(rootSeed /*, "bchtest" */) const change = this.bchjs.HDNode.derivePath( masterHDNode, walletInfo.hdPath ) const keyPair = this.bchjs.HDNode.toKeyPair(change) return keyPair } else { // No mnemonic. Wallet generated from WIF private key. const wif = walletInfo.privateKey if (!wif) { throw new Error('Wallet has no mnemonic or private key!') } const ecPair = this.bchjs.ECPair.fromWIF(walletInfo.privateKey) return ecPair } } // Top-level function that orchestrates the sending of BCH. // Expects an array of BCH-only UTXOs. Does not check if UTXOs belong to a token, // and will burn tokens if they are included in the UTXO list. async sendAllBch (toAddress, walletInfo, utxos) { try { // Generate the transaction. const transaction = await this.createSendAllTx( toAddress, walletInfo, utxos ) // console.log('transaction hex: ', transaction.hex) // Broadcast the transaction to the network. const txid = await this.ar.sendTx(transaction.hex) // console.log(txid) // TODO: Remove the spent UTXOs from the utxoStore. return txid } catch (err) { console.error('Error in send-bch.js/sendAllBch()') throw err } } // Expects an array of BCH-only UTXOs. Does not check if UTXOs belong to a token, // and will burn tokens if they are included in the UTXO list. async createSendAllTx (toAddress, walletInfo, utxos) { try { // console.log(`walletInfo: ${JSON.stringify(walletInfo, null, 2)}`) // console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`) // Validate Inputs if (!toAddress || typeof toAddress !== 'string') { throw new Error('Address to send must be a bch address') } // If the BCH utxos array is still empty, then throw an error. if (!utxos || utxos.length === 0) { throw new Error('UTXO list is empty') } // Create an instance of the Transaction Builder. const transactionBuilder = new this.bchjs.TransactionBuilder() // Set default value of 1 sat-per-byte if fee not specified. if (!walletInfo.fee) walletInfo.fee = 1.0 const satsPerByte = walletInfo.fee let totalAmount = 0 // Calculate Fee let fee = this.calculateFee(0, 2, satsPerByte) fee += PSF_DONATION // Add inputs utxos.forEach(utxo => { totalAmount += utxo.value // Additional cost per Utxo input is 148 sats for mining fees. fee += 148 transactionBuilder.addInput(utxo.tx_hash, utxo.tx_pos) }) // console.log(`totalAmount: ${totalAmount}`) // console.log(`fee: ${fee}`) // Add outputs transactionBuilder.addOutput(toAddress, totalAmount - fee) // Send a 2000 sat donation to PSF to thank them for creating this awesome software. transactionBuilder.addOutput(bchDonation('psf').donations, PSF_DONATION) // Generate a key pair from the mnemonic. const keyPair = await this.getKeyPairFromMnemonic(walletInfo) // Sign each UTXO that is about to be spent. utxos.forEach((utxo, i) => { let redeemScript transactionBuilder.sign( i, keyPair, redeemScript, transactionBuilder.hashTypes.SIGHASH_ALL, utxo.value ) }) // Build the transaction, return the compiled transaction in hex format. const tx = transactionBuilder.build() return { hex: tx.toHex(), txid: tx.getId() } } catch (err) { console.error('Error in send-bch.js/createSendAllTx()') throw err } } } module.exports = SendBCH