use crate::{
error::*,
transactions::{utils, CPFP_MIN_CHANGE},
txins::*,
txouts::*,
};
use miniscript::bitcoin::{
consensus::encode,
util::psbt::{Global as PsbtGlobal, Input as PsbtIn, PartiallySignedTransaction as Psbt},
Amount, Script, SigHashType, Transaction, TxIn,
};
use std::{convert::TryInto, fmt};
const OP_RETURN_DUMMY_DATA: [u8; 22] = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];
fn op_return_script(cpfp_psbt: &Psbt) -> Script {
if cpfp_psbt.global.unsigned_tx.input.len() > 1 {
Script::new_op_return(&[])
} else {
Script::new_op_return(&OP_RETURN_DUMMY_DATA)
}
}
#[derive(Clone, PartialEq)]
pub struct CpfpTransaction(Psbt);
impl fmt::Debug for CpfpTransaction {
fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", base64::encode(encode::serialize(&self.0)))
}
}
impl fmt::Display for CpfpTransaction {
fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result {
write!(f, "{:?}", &self)
}
}
impl CpfpTransaction {
pub fn from_txins(
to_be_cpfped: Vec<CpfpTxIn>,
tbc_weight: u64,
tbc_fees: Amount,
added_feerate: u64,
mut available_utxos: Vec<CpfpTxIn>,
) -> Result<CpfpTransaction, TransactionCreationError> {
assert!(!to_be_cpfped.is_empty());
available_utxos.sort_unstable_by_key(|l| l.txout().txout().value);
let mut txins = vec![];
let mut psbtins = vec![];
let mut dummy_change = None;
let mut inputs_sum = Amount::from_sat(0);
let mut total_satisfation_weight = 0;
for cpfp_txin in to_be_cpfped {
dummy_change = Some(cpfp_txin.txout().txout().clone());
inputs_sum += Amount::from_sat(cpfp_txin.txout().txout().value);
let w: u64 = cpfp_txin
.txout()
.max_sat_weight()
.try_into()
.expect("Weight doesn't fit in u64?");
total_satisfation_weight += w;
txins.push(TxIn {
previous_output: cpfp_txin.outpoint(),
sequence: RBF_SEQUENCE,
script_sig: Script::new(),
witness: vec![],
});
psbtins.push(PsbtIn {
witness_script: Some(cpfp_txin.txout().witness_script().clone()),
bip32_derivation: cpfp_txin.txout().bip32_derivation().clone(),
sighash_type: Some(SigHashType::All),
witness_utxo: Some(cpfp_txin.into_txout().into_txout()),
..PsbtIn::default()
});
}
let dummy_change = dummy_change.expect("Must be initialized in the loop");
let transaction = Transaction {
version: 2,
lock_time: 0,
input: txins,
output: vec![dummy_change],
};
let mut psbt = Psbt {
global: PsbtGlobal::from_unsigned_tx(transaction).expect("unsigned"),
inputs: psbtins,
outputs: vec![Default::default()],
};
let tbc_feerate = 1_000 * (tbc_fees.as_sat() + tbc_weight) / tbc_weight;
let target_feerate = tbc_feerate + added_feerate;
loop {
let cpfp_weight: u64 = psbt
.global
.unsigned_tx
.get_weight()
.try_into()
.expect("Weight doesn't fit in u64?");
let package_weight = cpfp_weight + total_satisfation_weight + tbc_weight;
let fees_needed = Amount::from_sat(
target_feerate * package_weight / 1000,
) - tbc_fees;
let mut op_return_tx = psbt.global.unsigned_tx.clone();
op_return_tx.output[0].script_pubkey = op_return_script(&psbt);
op_return_tx.output[0].value = 0;
let opr_tx_weight: u64 = op_return_tx
.get_weight()
.try_into()
.expect("Weight doesn't fit in u64?");
let opr_package_weight = opr_tx_weight + total_satisfation_weight + tbc_weight;
let op_return_fees_needed = Amount::from_sat(
target_feerate * opr_package_weight / 1000,
) - tbc_fees;
if inputs_sum > fees_needed || inputs_sum > op_return_fees_needed {
if inputs_sum > fees_needed && (inputs_sum - fees_needed).as_sat() > CPFP_MIN_CHANGE
{
let change = &mut psbt.global.unsigned_tx.output[0];
change.value = (inputs_sum - fees_needed).as_sat();
} else {
let opr = op_return_script(&psbt);
let change = &mut psbt.global.unsigned_tx.output[0];
change.value = 0;
change.script_pubkey = opr;
}
return Ok(CpfpTransaction(psbt));
} else {
match available_utxos.pop() {
Some(new_input) => {
psbt.global.unsigned_tx.input.push(TxIn {
previous_output: new_input.outpoint(),
script_sig: Script::new(),
sequence: RBF_SEQUENCE,
witness: vec![],
});
let bip32_derivation = new_input.txout().clone().into_bip32_derivation();
psbt.inputs.push(PsbtIn {
witness_utxo: Some(new_input.txout().txout().clone()),
witness_script: Some(new_input.txout().clone().into_witness_script()),
sighash_type: Some(SigHashType::All),
bip32_derivation,
..Default::default()
});
inputs_sum += Amount::from_sat(new_input.txout().txout().value);
let input_sat_weight: u64 = new_input
.txout()
.max_sat_weight()
.try_into()
.expect("Weight doesn't fit in u64?");
total_satisfation_weight += input_sat_weight;
}
None => {
return Err(TransactionCreationError::InsufficientFunds);
}
}
}
}
}
pub fn psbt(&self) -> &Psbt {
&self.0
}
pub fn into_psbt(self) -> Psbt {
self.0
}
pub fn tx(&self) -> &Transaction {
&self.psbt().global.unsigned_tx
}
pub fn fees(&self) -> Amount {
utils::psbt_fees(self.psbt()).expect("Fee computation bug: overflow")
}
}