1use crate::builder::ZincWallet;
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeSet;
4
5#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
7pub struct InscriptionDetails {
8 pub id: String,
10 pub number: i64,
12 pub content_type: Option<String>,
14}
15
16#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
18pub struct TxItem {
19 pub txid: String,
21 pub amount_sats: i64,
23 pub fee_sats: u64,
25 pub confirmation_time: Option<u64>,
27 pub tx_type: String, #[serde(default)]
30 pub inscriptions: Vec<InscriptionDetails>,
32 #[serde(default)]
33 pub parent_txids: Vec<String>,
35 pub index: usize,
37}
38
39impl ZincWallet {
40 pub fn get_transactions(&self, limit: usize) -> Vec<TxItem> {
42 let mut items = Vec::new();
43
44 self.collect_txs_from_wallet(&self.vault_wallet, &mut items);
46
47 if let Some(payment_wallet) = &self.payment_wallet {
49 self.collect_txs_from_wallet(payment_wallet, &mut items);
50 }
51
52 let mut combined: std::collections::HashMap<String, TxItem> =
54 std::collections::HashMap::new();
55 for item in items {
56 combined
57 .entry(item.txid.clone())
58 .and_modify(|existing| {
59 existing.amount_sats += item.amount_sats;
60 if item.confirmation_time > existing.confirmation_time {
61 existing.confirmation_time = item.confirmation_time;
62 }
63 if item.index > existing.index {
64 existing.index = item.index;
65 }
66 for new_ins in &item.inscriptions {
69 if !existing.inscriptions.iter().any(|e| e.id == new_ins.id) {
70 existing.inscriptions.push(new_ins.clone());
71 }
72 }
73 let mut merged_parent_txids: BTreeSet<String> =
75 existing.parent_txids.iter().cloned().collect();
76 merged_parent_txids.extend(item.parent_txids.iter().cloned());
77 existing.parent_txids = merged_parent_txids.into_iter().collect();
78 })
79 .or_insert(item);
80 }
81
82 let mut final_items: Vec<TxItem> = combined.into_values().collect();
83
84 final_items.sort_by(|a, b| {
86 let a_pending = a.confirmation_time.is_none();
87 let b_pending = b.confirmation_time.is_none();
88
89 if a_pending != b_pending {
90 return if a_pending {
91 std::cmp::Ordering::Less
92 } else {
93 std::cmp::Ordering::Greater
94 };
95 }
96
97
98 match (a.confirmation_time, b.confirmation_time) {
99 (Some(ta), Some(tb)) if ta != tb => tb.cmp(&ta), _ => {
101 let idx_order = b.index.cmp(&a.index);
103 if idx_order != std::cmp::Ordering::Equal {
104 idx_order
105 } else {
106 b.txid.cmp(&a.txid)
107 }
108 }
109 }
110 });
111
112 final_items.into_iter().take(limit).collect()
114 }
115
116 fn collect_txs_from_wallet(&self, wallet: &bdk_wallet::Wallet, items: &mut Vec<TxItem>) {
117 for (i, tx) in wallet.transactions().enumerate() {
118 let (sent, received) = wallet.sent_and_received(&tx.tx_node.tx);
119 #[allow(clippy::cast_possible_wrap)]
120 let amount_sats = received.to_sat() as i64 - sent.to_sat() as i64;
121
122 let fee_sats = wallet
123 .calculate_fee(&tx.tx_node.tx)
124 .map(|f| f.to_sat())
125 .unwrap_or(0);
126
127 let confirmation_time = match tx.chain_position {
128 bdk_chain::ChainPosition::Confirmed { anchor, .. } => {
129 Some(anchor.confirmation_time)
130 }
131 bdk_chain::ChainPosition::Unconfirmed { .. } => None,
132 };
133
134 let inscriptions = self.get_inscription_details(&tx.tx_node.tx, tx.tx_node.txid);
135 let parent_txids = tx
136 .tx_node
137 .tx
138 .input
139 .iter()
140 .map(|input| input.previous_output.txid.to_string())
141 .collect::<BTreeSet<_>>()
142 .into_iter()
143 .collect::<Vec<_>>();
144
145 items.push(TxItem {
146 txid: tx.tx_node.txid.to_string(),
147 amount_sats,
148 fee_sats,
149 confirmation_time,
150 tx_type: if amount_sats >= 0 {
151 "receive".to_string()
152 } else {
153 "send".to_string()
154 },
155 inscriptions,
156 parent_txids,
157 index: i,
158 });
159 }
160 }
161
162 fn get_inscription_details(
163 &self,
164 tx: &bitcoin::Transaction,
165 txid: bitcoin::Txid,
166 ) -> Vec<InscriptionDetails> {
167 let mut results = Vec::new();
168 for (i, _) in tx.output.iter().enumerate() {
169 let outpoint = bitcoin::OutPoint::new(txid, u32::try_from(i).unwrap());
170 if let Some(ins) = self
172 .inscriptions
173 .iter()
174 .find(|ins| ins.satpoint.outpoint == outpoint)
175 {
176 results.push(InscriptionDetails {
177 id: ins.id.clone(),
178 number: ins.number,
179 content_type: ins.content_type.clone(),
180 });
181 }
182 }
183 results
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use crate::builder::{Seed64, WalletBuilder};
190 use bitcoin::Network;
191
192 #[test]
193 fn test_get_transactions_empty() {
194 let seed = [0u8; 64];
195 let wallet = WalletBuilder::from_seed(Network::Regtest, Seed64::from_array(seed))
196 .build()
197 .unwrap();
198 let txs = wallet.get_transactions(50);
199 assert_eq!(txs.len(), 0);
200 }
201}