titan_types/
transaction.rs1use {
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}