use {
crate::{tx_in::TxIn, tx_out::SpentStatus, TxOut},
bitcoin::{constants::WITNESS_SCALE_FACTOR, BlockHash, Txid},
serde::{Deserialize, Serialize},
};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TransactionStatus {
pub confirmed: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub block_height: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub block_hash: Option<BlockHash>,
}
impl TransactionStatus {
pub fn unconfirmed() -> Self {
Self {
confirmed: false,
block_height: None,
block_hash: None,
}
}
pub fn confirmed(block_height: u64, block_hash: BlockHash) -> Self {
Self {
confirmed: true,
block_height: Some(block_height),
block_hash: Some(block_hash),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Transaction {
pub txid: Txid,
pub version: i32,
pub lock_time: u32,
pub input: Vec<TxIn>,
pub output: Vec<TxOut>,
pub status: TransactionStatus,
pub size: u64,
pub weight: u64,
}
impl Transaction {
pub fn vbytes(&self) -> u64 {
(self.weight + WITNESS_SCALE_FACTOR as u64 - 1) / WITNESS_SCALE_FACTOR as u64
}
pub fn is_coinbase(&self) -> bool {
if self.input.len() != 1 {
return false;
}
let prev = &self.input[0].previous_output;
let is_zero_txid = prev.txid().iter().all(|b| *b == 0);
let is_max_vout = prev.vout() == u32::MAX;
is_zero_txid && is_max_vout
}
pub fn input_value_sat(&self) -> Option<u64> {
let mut sum: u64 = 0;
for txin in &self.input {
let value = txin.previous_output_data.as_ref()?.value;
sum = sum.saturating_add(value);
}
Some(sum)
}
pub fn output_value_sat(&self) -> u64 {
self.output
.iter()
.fold(0u64, |acc, o| acc.saturating_add(o.value))
}
pub fn fee_paid_sat(&self) -> Option<u64> {
if self.is_coinbase() {
return None;
}
let input_sum = self.input_value_sat()?;
input_sum.checked_sub(self.output_value_sat())
}
pub fn fee_rate_sat_vb(&self) -> Option<f64> {
let fee = self.fee_paid_sat()? as f64;
let vbytes = self.vbytes() as f64;
if vbytes == 0.0 {
return None;
}
Some(fee / vbytes)
}
pub fn num_inputs(&self) -> usize {
self.input.len()
}
pub fn num_outputs(&self) -> usize {
self.output.len()
}
pub fn has_runes(&self) -> bool {
self.output.iter().any(|o| !o.runes.is_empty())
}
pub fn has_risky_runes(&self) -> bool {
self.output.iter().any(|o| !o.risky_runes.is_empty())
}
}
impl
From<(
bitcoin::Transaction,
TransactionStatus,
Vec<Option<TxOut>>,
Vec<Option<TxOut>>,
)> for Transaction
{
fn from(
(transaction, status, prev_outputs, outputs): (
bitcoin::Transaction,
TransactionStatus,
Vec<Option<TxOut>>,
Vec<Option<TxOut>>,
),
) -> Self {
Transaction {
size: transaction.total_size() as u64,
weight: transaction.weight().to_wu(),
txid: transaction.compute_txid(),
version: transaction.version.0,
lock_time: transaction.lock_time.to_consensus_u32(),
input: transaction
.input
.into_iter()
.zip(prev_outputs.into_iter())
.map(|(tx_in, prev_output)| TxIn::from((tx_in, prev_output)))
.collect(),
output: transaction
.output
.into_iter()
.zip(outputs.into_iter())
.map(|(tx_out, tx_out_entry)| {
let (runes, risky_runes, spent) = match tx_out_entry {
Some(tx_out_entry) => (
tx_out_entry.runes,
tx_out_entry.risky_runes,
tx_out_entry.spent,
),
None => (vec![], vec![], SpentStatus::SpentUnknown),
};
let tx_out = TxOut {
value: tx_out.value.to_sat(),
script_pubkey: tx_out.script_pubkey,
runes,
risky_runes,
spent,
};
tx_out
})
.collect(),
status,
}
}
}