use crate::{
error::*,
transactions::{TX_LOCKTIME, TX_VERSION},
txins::RevaultTxIn,
txouts::{RevaultInternalTxOut, RevaultTxOut},
};
use miniscript::bitcoin::{
blockdata::constants::max_money,
util::psbt::{Global as PsbtGlobal, Input as PsbtIn, PartiallySignedTransaction as Psbt},
Amount, Network, OutPoint, Transaction,
};
use std::collections::{BTreeMap, HashSet};
macro_rules! impl_revault_transaction {
( $transaction_name:ident, $doc_comment:meta ) => {
use crate::transactions::inner_mut;
use std::{fmt, str};
#[$doc_comment]
#[derive(Debug, Clone, PartialEq)]
pub struct $transaction_name(Psbt);
impl inner_mut::PrivateInnerMut for $transaction_name {
fn psbt(&self) -> &Psbt {
&self.0
}
fn psbt_mut(&mut self) -> &mut Psbt {
&mut self.0
}
fn into_psbt(self) -> Psbt {
self.0
}
fn from_psbt_serialized(
raw_psbt: &[u8],
) -> Result<Self, TransactionSerialisationError> {
$transaction_name::from_raw_psbt(raw_psbt)
}
}
#[cfg(feature = "use-serde")]
impl Serialize for $transaction_name {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if serializer.is_human_readable() {
serializer.serialize_str(&self.as_psbt_string())
} else {
serializer.serialize_bytes(&self.as_psbt_serialized())
}
}
}
#[cfg(feature = "use-serde")]
impl<'de> Deserialize<'de> for $transaction_name {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
if deserializer.is_human_readable() {
$transaction_name::from_psbt_str(&String::deserialize(deserializer)?)
.map_err(de::Error::custom)
} else {
$transaction_name::from_psbt_serialized(&Vec::<u8>::deserialize(deserializer)?)
.map_err(de::Error::custom)
}
}
}
impl fmt::Display for $transaction_name {
fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.as_psbt_string())
}
}
impl str::FromStr for $transaction_name {
type Err = TransactionSerialisationError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
$transaction_name::from_psbt_str(s)
}
}
};
}
pub fn psbt_common_sanity_checks(psbt: Psbt) -> Result<Psbt, PsbtValidationError> {
let inner_tx = &psbt.global.unsigned_tx;
if inner_tx.version != TX_VERSION {
return Err(PsbtValidationError::InvalidTransactionVersion(
inner_tx.version,
));
}
let input_count = inner_tx.input.len();
let psbt_input_count = psbt.inputs.len();
if input_count != psbt_input_count {
return Err(PsbtValidationError::InputCountMismatch(
input_count,
psbt_input_count,
));
}
let output_count = inner_tx.output.len();
let psbt_output_count = psbt.outputs.len();
if output_count != psbt_output_count {
return Err(PsbtValidationError::OutputCountMismatch(
output_count,
psbt_output_count,
));
}
let uniq_txins: HashSet<OutPoint> = inner_tx.input.iter().map(|i| i.previous_output).collect();
if uniq_txins.len() != input_count {
return Err(PsbtValidationError::DuplicatedInput);
}
let mut is_final = None;
let mut value_in: u64 = 0;
for input in psbt.inputs.iter() {
if input.witness_utxo.is_none() {
return Err(PsbtValidationError::MissingWitnessUtxo(input.clone()));
}
if input.non_witness_utxo.is_some() {
return Err(PsbtValidationError::InvalidInputField(input.clone()));
}
if input.redeem_script.is_some() {
return Err(PsbtValidationError::InvalidInputField(input.clone()));
}
if input.final_script_witness.is_some() {
if is_final == Some(false) || input.witness_script.is_some() {
return Err(PsbtValidationError::PartiallyFinalized);
}
is_final = Some(true);
} else {
if is_final == Some(true) {
return Err(PsbtValidationError::PartiallyFinalized);
}
is_final = Some(false);
}
if let Some(ref script) = input.witness_script {
let _: miniscript::Miniscript<_, miniscript::Segwitv0> =
miniscript::Miniscript::parse(script)
.map_err(|_| PsbtValidationError::InvalidInWitnessScript(input.clone()))?;
}
let spent_utxo_value = input
.witness_utxo
.as_ref()
.expect("None checked above")
.value;
if spent_utxo_value > max_money(Network::Bitcoin) {
return Err(PsbtValidationError::InsaneAmounts);
}
value_in = value_in
.checked_add(spent_utxo_value)
.ok_or(PsbtValidationError::InsaneAmounts)?;
let spk = &input.witness_utxo.as_ref().unwrap().script_pubkey;
if !spk.is_v0_p2wsh() {
return Err(PsbtValidationError::InvalidInputField(input.clone()));
}
if is_final == Some(true) {
continue;
}
if input.bip32_derivation.is_empty() {
return Err(PsbtValidationError::InvalidInputField(input.clone()));
}
let ws = input
.witness_script
.as_ref()
.ok_or_else(|| PsbtValidationError::MissingInWitnessScript(input.clone()))?;
if &ws.to_v0_p2wsh() != spk {
return Err(PsbtValidationError::InvalidInWitnessScript(input.clone()));
}
}
let mut value_out: u64 = 0;
for o in inner_tx.output.iter() {
if o.value > max_money(Network::Bitcoin) || o.value < o.script_pubkey.dust_value().as_sat()
{
return Err(PsbtValidationError::InsaneAmounts);
}
value_out = value_out
.checked_add(o.value)
.ok_or(PsbtValidationError::InsaneAmounts)?;
}
if value_out > value_in {
return Err(PsbtValidationError::InsaneAmounts);
}
if value_in - value_out > max_money(Network::Bitcoin) {
return Err(PsbtValidationError::InsaneAmounts);
}
Ok(psbt)
}
pub fn psbt_fees(psbt: &Psbt) -> Option<Amount> {
let mut value_in = Amount::from_sat(0);
for i in psbt.inputs.iter() {
value_in = value_in.checked_add(Amount::from_sat(i.witness_utxo.as_ref()?.value))?;
}
let mut value_out = Amount::from_sat(0);
for o in psbt.global.unsigned_tx.output.iter() {
value_out = value_out.checked_add(Amount::from_sat(o.value))?
}
value_in.checked_sub(value_out)
}
pub fn create_psbt<Out: RevaultTxOut, IntOut: RevaultInternalTxOut, In: RevaultTxIn<IntOut>>(
txin: In,
txo: Out,
) -> Psbt {
let input = vec![txin.unsigned_txin()];
let psbtins = vec![PsbtIn {
witness_script: Some(txin.txout().witness_script().clone()),
bip32_derivation: txin.txout().bip32_derivation().clone(),
witness_utxo: Some(txin.into_txout().into_txout()),
..PsbtIn::default()
}];
let psbtouts = vec![txo.psbtout()];
let output = vec![txo.into_txout()];
Psbt {
global: PsbtGlobal {
unsigned_tx: Transaction {
version: TX_VERSION,
lock_time: TX_LOCKTIME,
input,
output,
},
version: 0,
xpub: BTreeMap::new(),
proprietary: BTreeMap::new(),
unknown: BTreeMap::new(),
},
inputs: psbtins,
outputs: psbtouts,
}
}