use crate::{
error::*,
scripts::*,
transactions::{
utils, CpfpableTransaction, RevaultPresignedTransaction, RevaultTransaction,
DEPOSIT_MIN_SATS, INSANE_FEES, MAX_STANDARD_TX_WEIGHT, TX_LOCKTIME, TX_VERSION,
UNVAULT_CPFP_VALUE, UNVAULT_TX_FEERATE,
},
txins::*,
txouts::*,
};
use miniscript::{
bitcoin::{
blockdata::constants::max_money,
consensus::encode::Decodable,
util::psbt::{
Global as PsbtGlobal, Input as PsbtIn, Output as PsbtOut,
PartiallySignedTransaction as Psbt,
},
Amount, Network, OutPoint, Transaction,
},
DescriptorTrait,
};
#[cfg(feature = "use-serde")]
use {
serde::de::{self, Deserialize, Deserializer},
serde::ser::{Serialize, Serializer},
};
use std::{collections::BTreeMap, convert::TryInto};
impl_revault_transaction!(
UnvaultTransaction,
doc = "The unvaulting transaction, spending a deposit and being eventually spent by a spend transaction (if not revaulted)."
);
impl RevaultPresignedTransaction for UnvaultTransaction {}
impl UnvaultTransaction {
fn create_psbt(
deposit_txin: DepositTxIn,
unvault_txout: UnvaultTxOut,
cpfp_txout: CpfpTxOut,
) -> Psbt {
Psbt {
outputs: vec![
PsbtOut {
bip32_derivation: unvault_txout.bip32_derivation().clone(),
..PsbtOut::default()
},
PsbtOut {
bip32_derivation: cpfp_txout.bip32_derivation().clone(),
..PsbtOut::default()
},
],
global: PsbtGlobal {
unsigned_tx: Transaction {
version: TX_VERSION,
lock_time: TX_LOCKTIME,
input: vec![deposit_txin.unsigned_txin()],
output: vec![unvault_txout.into_txout(), cpfp_txout.into_txout()],
},
version: 0,
xpub: BTreeMap::new(),
proprietary: BTreeMap::new(),
unknown: BTreeMap::new(),
},
inputs: vec![PsbtIn {
witness_script: Some(deposit_txin.txout().witness_script().clone()),
bip32_derivation: deposit_txin.txout().bip32_derivation().clone(),
witness_utxo: Some(deposit_txin.into_txout().into_txout()),
..PsbtIn::default()
}],
}
}
pub fn new(
deposit_input: DepositTxIn,
unvault_descriptor: &DerivedUnvaultDescriptor,
cpfp_descriptor: &DerivedCpfpDescriptor,
) -> Result<UnvaultTransaction, TransactionCreationError> {
let dummy_unvault_txout = UnvaultTxOut::new(Amount::from_sat(u64::MAX), unvault_descriptor);
let dummy_cpfp_txout = CpfpTxOut::new(Amount::from_sat(u64::MAX), cpfp_descriptor);
let dummy_tx = UnvaultTransaction::create_psbt(
deposit_input.clone(),
dummy_unvault_txout,
dummy_cpfp_txout,
)
.global
.unsigned_tx;
let total_weight = dummy_tx
.get_weight()
.checked_add(deposit_input.txout().max_sat_weight())
.expect("Properly-computed weights cannot overflow");
let total_weight: u64 = total_weight.try_into().expect("usize in u64");
let fees = UNVAULT_TX_FEERATE
.checked_mul(total_weight)
.expect("Properly-computed weights cannot overflow");
if fees > INSANE_FEES {
return Err(TransactionCreationError::InsaneFees);
}
assert!(
total_weight <= MAX_STANDARD_TX_WEIGHT as u64,
"A single input and two outputs"
);
let deposit_value = deposit_input.txout().txout().value;
if fees + UNVAULT_CPFP_VALUE + DEPOSIT_MIN_SATS > deposit_value {
return Err(TransactionCreationError::Dust);
}
let unvault_value = deposit_value - fees - UNVAULT_CPFP_VALUE; if unvault_value > max_money(Network::Bitcoin) {
return Err(TransactionCreationError::InsaneAmounts);
}
let unvault_txout = UnvaultTxOut::new(Amount::from_sat(unvault_value), unvault_descriptor);
let cpfp_txout = CpfpTxOut::new(Amount::from_sat(UNVAULT_CPFP_VALUE), cpfp_descriptor);
Ok(UnvaultTransaction(UnvaultTransaction::create_psbt(
deposit_input,
unvault_txout,
cpfp_txout,
)))
}
fn unvault_txin(
&self,
unvault_descriptor: &DerivedUnvaultDescriptor,
sequence: u32,
) -> UnvaultTxIn {
let spk = unvault_descriptor.inner().script_pubkey();
let index = self
.psbt()
.global
.unsigned_tx
.output
.iter()
.position(|txo| txo.script_pubkey == spk)
.expect("UnvaultTransaction is always created with an Unvault txo");
let txo = &self.psbt().global.unsigned_tx.output[index];
let prev_txout = UnvaultTxOut::new(Amount::from_sat(txo.value), unvault_descriptor);
UnvaultTxIn::new(
OutPoint {
txid: self.psbt().global.unsigned_tx.txid(),
vout: index.try_into().expect("There are two outputs"),
},
prev_txout,
sequence,
)
}
pub fn spend_unvault_txin(&self, unvault_descriptor: &DerivedUnvaultDescriptor) -> UnvaultTxIn {
self.unvault_txin(unvault_descriptor, unvault_descriptor.csv_value())
}
pub fn revault_unvault_txin(
&self,
unvault_descriptor: &DerivedUnvaultDescriptor,
) -> UnvaultTxIn {
self.unvault_txin(unvault_descriptor, RBF_SEQUENCE)
}
pub fn from_raw_psbt(raw_psbt: &[u8]) -> Result<Self, TransactionSerialisationError> {
let psbt = Decodable::consensus_decode(raw_psbt)?;
let psbt = utils::psbt_common_sanity_checks(psbt)?;
let output_count = psbt.global.unsigned_tx.output.len();
if output_count != 2 {
return Err(PsbtValidationError::InvalidOutputCount(output_count).into());
}
for output in psbt.outputs.iter() {
if output.bip32_derivation.is_empty() {
return Err(PsbtValidationError::InvalidOutputField(output.clone()).into());
}
}
let input_count = psbt.global.unsigned_tx.input.len();
if input_count != 1 {
return Err(PsbtValidationError::InvalidInputCount(input_count).into());
}
Ok(UnvaultTransaction(psbt))
}
}
impl CpfpableTransaction for UnvaultTransaction {
fn max_weight(&self) -> u64 {
let psbt = self.psbt();
let tx = &psbt.global.unsigned_tx;
let txin = &psbt.inputs[0];
let txin_weight: u64 = if self.is_finalized() {
txin.final_script_witness
.as_ref()
.expect("Always set if final")
.iter()
.map(|e| e.len())
.sum::<usize>()
.try_into()
.expect("Bug: witness size >u64::MAX")
} else {
miniscript::descriptor::Wsh::new(
miniscript::Miniscript::parse(
txin.witness_script
.as_ref()
.expect("Unvault txins always have a witness Script"),
)
.expect("UnvaultTxIn witness_script is created from a Miniscript"),
)
.expect("")
.max_satisfaction_weight()
.expect("It's a sane Script, derived from a Miniscript")
.try_into()
.expect("Can't be >u64::MAX")
};
let weight: u64 = tx.get_weight().try_into().expect("Can't be >u64::MAX");
let weight = weight + txin_weight;
assert!(weight > 0, "We never create an empty tx");
weight
}
}