minimal-slp-wallet
Version:
A minimalist Bitcoin Cash (BCH) wallet npm library, for use in a web apps.
273 lines (227 loc) • 8.69 kB
JavaScript
/*
This library contains utility functions for crafting transactions that
contain an OP_RETURN output.
*/
// Public npm libraries
const bchDonation = require('bch-donation')
// Local libraries
const SendBCH = require('./send-bch')
const PSF_DONATION = 2000
class OpReturn {
constructor (localConfig = {}) {
// Dependency injection.
this.bchjs = localConfig.bchjs
if (!this.bchjs) {
throw new Error(
'Must pass instance of bch-js when instantiating OpReturn library.'
)
}
this.ar = localConfig.ar
if (!this.ar) {
throw new Error('Must pass instance of Adapter Router.')
}
// Encapsulate dependencies.
// this.bchjs = new BCHJS(localConfig)
this.sendBch = new SendBCH(localConfig)
// this.utxos = new Utxos(localConfig)
// this.ar = new AdapterRouter({ bchjs: this.bchjs })
}
// Calculate the miner fee that needs to be paid for this transaction.
// Takes the size of the OP_RETURN buffer size into account.
calculateFee (numInputs, numOutputs, bufSize = 3, satsPerByte) {
try {
const byteCount = this.bchjs.BitcoinCash.getByteCount(
{ P2PKH: numInputs },
{ P2PKH: numOutputs + 1 }
)
// console.log(`satsPerByte: ${satsPerByte}`)
let fee = Math.ceil(byteCount * satsPerByte)
// console.log(`fee before op_return: ${fee}`)
// Adjust for the buffer size of the op_return
const minOpReturnFee = 10
fee = fee + minOpReturnFee + Math.ceil(bufSize * satsPerByte)
// console.log(`Fee with an OP_RETURN buffer size of ${bufSize}: ${fee}`)
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
}
}
// 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,
bufLen,
satsPerByte = 1.0
) {
const sortedUtxos = this.sendBch.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, bufLen, 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
// console.log(`satoshis needed: ${satoshisNeeded}`)
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 }
}
// Build the transaction for sending a TX with an OP_RETURN output.
// Note: SLP token UTXOs should not be sent to this function. They will be
// burned.
// An optional prefix is expected, as a hex string. If not provided, it will
// default to the memo.cash prefix for posting a 'tweet'.
//
// Other outputs can be added for sending BCH to. This can be useful for signaling.
// The bchOutput array should contain objects with a 'address' and 'amountSat'
// properties.
//
// The sats-per-byte can be increased from the default of 1, for times when
// the blockchain is congested and a fee market occurs.
async createTransaction (
walletInfo,
bchUtxos,
msg = '', // OP_RETURN data in utf8 string format.
prefix = '6d02', // Hex prefix. Replace with Lokad ID or memo.cash prefix.
bchOutput = [], // Array of objects with address and amountSat property
satsPerByte = 1.0
) {
try {
// console.log('createTransaction() start tokenUtxos: ', tokenUtxos)
// If the BCH utxos array is still empty, then throw an error.
if (!bchUtxos || bchUtxos.length === 0) {
throw new Error('BCH UTXO list is empty')
}
// Generate the OP_RETURN data.
const script = [
this.bchjs.Script.opcodes.OP_RETURN,
Buffer.from(prefix, 'hex'),
Buffer.from(msg)
]
const data = this.bchjs.Script.encode2(script)
// console.log('data.length: ', data.length)
// Generate the BCH output object.
// const bchOutput = [
// {
// address: walletInfo.cashAddress,
// amountSat: 550 // dust
// }
// ]
// 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(
bchOutput,
bchUtxos,
data.length,
satsPerByte
)
// Create an instance of the Transaction Builder.
const transactionBuilder = new this.bchjs.TransactionBuilder()
// Add BCH inputs
necessaryUtxos.forEach(utxo => {
transactionBuilder.addInput(utxo.tx_hash, utxo.tx_pos)
})
// console.log('tokens.js tokenUtxos: ', tokenUtxos)
// Add the OP_RETURN as the second output of the transaction.
transactionBuilder.addOutput(data, 0)
// 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}`)
// console.log(`walletInfo.cashAddress: ${walletInfo.cashAddress}`)
if (change && change > 546) {
transactionBuilder.addOutput(walletInfo.cashAddress, change)
}
// Add any additional outputs specified by the user.
for (let i = 0; i < bchOutput.length; i++) {
const thisOutput = bchOutput[i]
transactionBuilder.addOutput(thisOutput.address, thisOutput.amountSat)
}
// Generate a key pair from the mnemonic.
const keyPair = await this.sendBch.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 op-return.js/createTransaction()')
throw err
}
}
// Generate and broadcast a TX with an OP_RETURN output. Returns the TXID
// of the transaction.
async sendOpReturn (
wallet,
bchUtxos,
msg = '',
prefix = '6d02', // Default to memo.cash
bchOutput = [],
satsPerByte = 1.0
) {
const { hex } = await this.createTransaction(
wallet,
bchUtxos,
msg,
prefix,
bchOutput,
satsPerByte
)
// Broadcast the transaction to the network.
const txid = await this.ar.sendTx(hex)
return txid
}
}
module.exports = OpReturn