use alloc::boxed::Box;
use chain::{ChainPosition, ConfirmationBlockTime};
use core::convert::AsRef;
use core::fmt;
use bitcoin::transaction::{OutPoint, Sequence, TxOut};
use bitcoin::{psbt, Weight};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub enum KeychainKind {
External = 0,
Internal = 1,
}
impl KeychainKind {
pub fn as_byte(&self) -> u8 {
match self {
KeychainKind::External => b'e',
KeychainKind::Internal => b'i',
}
}
}
impl fmt::Display for KeychainKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
KeychainKind::External => write!(f, "External"),
KeychainKind::Internal => write!(f, "Internal"),
}
}
}
impl AsRef<[u8]> for KeychainKind {
fn as_ref(&self) -> &[u8] {
match self {
KeychainKind::External => b"e",
KeychainKind::Internal => b"i",
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
pub struct LocalOutput {
pub outpoint: OutPoint,
pub txout: TxOut,
pub keychain: KeychainKind,
pub is_spent: bool,
pub derivation_index: u32,
pub chain_position: ChainPosition<ConfirmationBlockTime>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WeightedUtxo {
pub satisfaction_weight: Weight,
pub utxo: Utxo,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Utxo {
Local(LocalOutput),
Foreign {
outpoint: OutPoint,
sequence: Sequence,
psbt_input: Box<psbt::Input>,
},
}
impl Utxo {
pub fn outpoint(&self) -> OutPoint {
match &self {
Utxo::Local(local) => local.outpoint,
Utxo::Foreign { outpoint, .. } => *outpoint,
}
}
pub fn txout(&self) -> &TxOut {
match &self {
Utxo::Local(local) => &local.txout,
Utxo::Foreign {
outpoint,
psbt_input,
..
} => psbt_input.witness_utxo.as_ref().unwrap_or_else(|| {
psbt_input
.non_witness_utxo
.as_ref()
.and_then(|tx| tx.output.get(outpoint.vout as usize))
.expect("Foreign UTXOs should have one of witness_utxo, non_witness_utxo set")
}),
}
}
pub fn sequence(&self) -> Option<Sequence> {
match self {
Utxo::Local(_) => None,
Utxo::Foreign { sequence, .. } => Some(*sequence),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IndexOutOfBoundsError {
pub index: usize,
pub len: usize,
}
impl IndexOutOfBoundsError {
pub fn new(index: usize, len: usize) -> Self {
Self { index, len }
}
}
impl fmt::Display for IndexOutOfBoundsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Index out of bounds: index {} is greater than or equal to length {}",
self.index, self.len
)
}
}
impl core::error::Error for IndexOutOfBoundsError {}
#[cfg(test)]
mod tests {
use super::*;
use bitcoin::{
absolute, transaction, Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut,
Witness,
};
fn build_tx(txout: TxOut) -> Transaction {
Transaction {
version: transaction::Version::TWO,
lock_time: absolute::LockTime::ZERO,
input: vec![TxIn {
previous_output: OutPoint::null(),
script_sig: ScriptBuf::default(),
sequence: Sequence::MAX,
witness: Witness::default(),
}],
output: vec![txout],
}
}
#[test]
fn txout_foreign_returns_witness_utxo() {
let txout = TxOut {
value: Amount::from_sat(100_000),
script_pubkey: ScriptBuf::default(),
};
let utxo = Utxo::Foreign {
outpoint: OutPoint::null(),
sequence: Sequence::MAX,
psbt_input: Box::new(psbt::Input {
witness_utxo: Some(txout.clone()),
..Default::default()
}),
};
assert_eq!(utxo.txout(), &txout);
}
#[test]
fn txout_foreign_returns_non_witness_utxo() {
let txout = TxOut {
value: Amount::from_sat(100_000),
script_pubkey: ScriptBuf::default(),
};
let prev_tx = build_tx(txout.clone());
let utxo = Utxo::Foreign {
outpoint: OutPoint {
txid: prev_tx.compute_txid(),
vout: 0,
},
sequence: Sequence::MAX,
psbt_input: Box::new(psbt::Input {
non_witness_utxo: Some(prev_tx),
..Default::default()
}),
};
assert_eq!(utxo.txout(), &txout);
}
#[test]
#[should_panic(
expected = "Foreign UTXOs should have one of witness_utxo, non_witness_utxo set"
)]
fn txout_foreign_panics_with_empty_psbt_input() {
let utxo = Utxo::Foreign {
outpoint: OutPoint::null(),
sequence: Sequence::MAX,
psbt_input: Box::new(psbt::Input::default()),
};
utxo.txout();
}
}