titan_types/
transaction.rs

1use {
2    crate::{tx_in::TxIn, tx_out::SpentStatus, TxOut},
3    bitcoin::{constants::WITNESS_SCALE_FACTOR, BlockHash, Txid},
4    serde::{Deserialize, Serialize},
5};
6
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
8pub struct TransactionStatus {
9    pub confirmed: bool,
10    #[serde(skip_serializing_if = "Option::is_none")]
11    pub block_height: Option<u64>,
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub block_hash: Option<BlockHash>,
14}
15
16impl TransactionStatus {
17    pub fn unconfirmed() -> Self {
18        Self {
19            confirmed: false,
20            block_height: None,
21            block_hash: None,
22        }
23    }
24
25    pub fn confirmed(block_height: u64, block_hash: BlockHash) -> Self {
26        Self {
27            confirmed: true,
28            block_height: Some(block_height),
29            block_hash: Some(block_hash),
30        }
31    }
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct Transaction {
36    pub txid: Txid,
37    pub version: i32,
38    pub lock_time: u32,
39    pub input: Vec<TxIn>,
40    pub output: Vec<TxOut>,
41    pub status: TransactionStatus,
42    pub size: u64,
43    pub weight: u64,
44}
45
46impl Transaction {
47    pub fn vbytes(&self) -> u64 {
48        (self.weight + WITNESS_SCALE_FACTOR as u64 - 1) / WITNESS_SCALE_FACTOR as u64
49    }
50
51    pub fn is_coinbase(&self) -> bool {
52        if self.input.len() != 1 {
53            return false;
54        }
55
56        let prev = &self.input[0].previous_output;
57        let is_zero_txid = prev.txid().iter().all(|b| *b == 0);
58        let is_max_vout = prev.vout() == u32::MAX;
59        is_zero_txid && is_max_vout
60    }
61
62    pub fn input_value_sat(&self) -> Option<u64> {
63        let mut sum: u64 = 0;
64
65        for txin in &self.input {
66            let value = txin.previous_output_data.as_ref()?.value;
67            sum = sum.saturating_add(value);
68        }
69
70        Some(sum)
71    }
72
73    pub fn output_value_sat(&self) -> u64 {
74        self.output
75            .iter()
76            .fold(0u64, |acc, o| acc.saturating_add(o.value))
77    }
78
79    pub fn fee_paid_sat(&self) -> Option<u64> {
80        if self.is_coinbase() {
81            return None;
82        }
83
84        let input_sum = self.input_value_sat()?;
85        input_sum.checked_sub(self.output_value_sat())
86    }
87
88    pub fn fee_rate_sat_vb(&self) -> Option<f64> {
89        let fee = self.fee_paid_sat()? as f64;
90        let vbytes = self.vbytes() as f64;
91        if vbytes == 0.0 {
92            return None;
93        }
94        Some(fee / vbytes)
95    }
96
97    pub fn num_inputs(&self) -> usize {
98        self.input.len()
99    }
100
101    pub fn num_outputs(&self) -> usize {
102        self.output.len()
103    }
104
105    pub fn has_runes(&self) -> bool {
106        self.output.iter().any(|o| !o.runes.is_empty())
107    }
108
109    pub fn has_risky_runes(&self) -> bool {
110        self.output.iter().any(|o| !o.risky_runes.is_empty())
111    }
112}
113
114impl
115    From<(
116        bitcoin::Transaction,
117        TransactionStatus,
118        Vec<Option<TxOut>>,
119        Vec<Option<TxOut>>,
120    )> for Transaction
121{
122    fn from(
123        (transaction, status, prev_outputs, outputs): (
124            bitcoin::Transaction,
125            TransactionStatus,
126            Vec<Option<TxOut>>,
127            Vec<Option<TxOut>>,
128        ),
129    ) -> Self {
130        Transaction {
131            size: transaction.total_size() as u64,
132            weight: transaction.weight().to_wu(),
133            txid: transaction.compute_txid(),
134            version: transaction.version.0,
135            lock_time: transaction.lock_time.to_consensus_u32(),
136            input: transaction
137                .input
138                .into_iter()
139                .zip(prev_outputs.into_iter())
140                .map(|(tx_in, prev_output)| TxIn::from((tx_in, prev_output)))
141                .collect(),
142            output: transaction
143                .output
144                .into_iter()
145                .zip(outputs.into_iter())
146                .map(|(tx_out, tx_out_entry)| {
147                    let (runes, risky_runes, spent) = match tx_out_entry {
148                        Some(tx_out_entry) => (
149                            tx_out_entry.runes,
150                            tx_out_entry.risky_runes,
151                            tx_out_entry.spent,
152                        ),
153                        None => (vec![], vec![], SpentStatus::SpentUnknown),
154                    };
155
156                    let tx_out = TxOut {
157                        value: tx_out.value.to_sat(),
158                        script_pubkey: tx_out.script_pubkey,
159                        runes,
160                        risky_runes,
161                        spent,
162                    };
163
164                    tx_out
165                })
166                .collect(),
167            status,
168        }
169    }
170}