1use std::collections::HashMap;
4
5use bdk_wallet::bitcoin::Address;
6use bdk_wallet::chain::ChainPosition;
7use bdk_wallet::KeychainKind;
8
9use crate::{CdkBdk, Error};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct WalletBalance {
14 pub network: String,
16 pub synced_height: u32,
18 pub confirmed_sat: u64,
20 pub trusted_pending_sat: u64,
22 pub untrusted_pending_sat: u64,
24 pub immature_sat: u64,
26 pub trusted_spendable_sat: u64,
28 pub total_sat: u64,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct WalletTransaction {
35 pub txid: String,
37 pub received_sat: u64,
39 pub sent_sat: u64,
41 pub fee_sat: Option<u64>,
43 pub balance_delta_sat: i64,
45 pub confirmation_height: Option<u32>,
47 pub confirmation_time: Option<u64>,
49 pub first_seen: Option<u64>,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
55pub enum WalletKeychain {
56 External,
58 Internal,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct WalletAddress {
65 pub address: String,
67 pub keychain: WalletKeychain,
69 pub derivation_index: u32,
71 pub used: bool,
73 pub balance_sat: u64,
75 pub confirmed_balance_sat: u64,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct WalletPage<T> {
82 pub items: Vec<T>,
84 pub total: u64,
86}
87
88impl CdkBdk {
89 pub async fn wallet_balance(&self) -> WalletBalance {
91 let wallet_with_db = self.wallet_with_db.lock().await;
92 let balance = wallet_with_db.wallet.balance();
93
94 WalletBalance {
95 network: self.network.to_string(),
96 synced_height: wallet_with_db.wallet.latest_checkpoint().height(),
97 confirmed_sat: balance.confirmed.to_sat(),
98 trusted_pending_sat: balance.trusted_pending.to_sat(),
99 untrusted_pending_sat: balance.untrusted_pending.to_sat(),
100 immature_sat: balance.immature.to_sat(),
101 trusted_spendable_sat: balance.trusted_spendable().to_sat(),
102 total_sat: balance.total().to_sat(),
103 }
104 }
105
106 pub async fn wallet_transactions(
108 &self,
109 offset: usize,
110 limit: usize,
111 ) -> Result<WalletPage<WalletTransaction>, Error> {
112 let wallet_with_db = self.wallet_with_db.lock().await;
113 let wallet = &wallet_with_db.wallet;
114 let transactions = wallet.transactions_sort_by(|left, right| {
115 right
116 .chain_position
117 .cmp(&left.chain_position)
118 .then_with(|| right.tx_node.txid.cmp(&left.tx_node.txid))
119 });
120 let total = u64::try_from(transactions.len())
121 .map_err(|_| Error::Wallet("Transaction count exceeds u64".to_string()))?;
122
123 let items = transactions
124 .into_iter()
125 .skip(offset)
126 .take(limit)
127 .map(|transaction| {
128 let tx = &transaction.tx_node.tx;
129 let (sent, received) = wallet.sent_and_received(tx);
130 let received_sat = received.to_sat();
131 let sent_sat = sent.to_sat();
132 let received_signed = i64::try_from(received_sat)
133 .map_err(|_| Error::Wallet("Received value exceeds i64".to_string()))?;
134 let sent_signed = i64::try_from(sent_sat)
135 .map_err(|_| Error::Wallet("Sent value exceeds i64".to_string()))?;
136
137 let (confirmation_height, confirmation_time, first_seen) =
138 match transaction.chain_position {
139 ChainPosition::Confirmed { anchor, .. } => (
140 Some(anchor.block_id.height),
141 Some(anchor.confirmation_time),
142 None,
143 ),
144 ChainPosition::Unconfirmed { first_seen, .. } => (None, None, first_seen),
145 };
146
147 Ok(WalletTransaction {
148 txid: transaction.tx_node.txid.to_string(),
149 received_sat,
150 sent_sat,
151 fee_sat: wallet.calculate_fee(tx).ok().map(|fee| fee.to_sat()),
152 balance_delta_sat: received_signed - sent_signed,
153 confirmation_height,
154 confirmation_time,
155 first_seen,
156 })
157 })
158 .collect::<Result<Vec<_>, Error>>()?;
159
160 Ok(WalletPage { items, total })
161 }
162
163 pub async fn wallet_addresses(
165 &self,
166 offset: usize,
167 limit: usize,
168 ) -> Result<WalletPage<WalletAddress>, Error> {
169 let wallet_with_db = self.wallet_with_db.lock().await;
170 let wallet = &wallet_with_db.wallet;
171 let mut balances = HashMap::<(KeychainKind, u32), (u64, u64)>::new();
172
173 for output in wallet.list_unspent() {
174 let entry = balances
175 .entry((output.keychain, output.derivation_index))
176 .or_default();
177 entry.0 = entry
178 .0
179 .checked_add(output.txout.value.to_sat())
180 .ok_or_else(|| Error::Wallet("Address balance overflow".to_string()))?;
181 if output.chain_position.is_confirmed() {
182 entry.1 = entry
183 .1
184 .checked_add(output.txout.value.to_sat())
185 .ok_or_else(|| Error::Wallet("Address balance overflow".to_string()))?;
186 }
187 }
188
189 let keychains = [
190 (KeychainKind::External, WalletKeychain::External),
191 (KeychainKind::Internal, WalletKeychain::Internal),
192 ];
193 let total = keychains.iter().try_fold(0_u64, |total, (keychain, _)| {
194 let keychain_total =
195 u64::try_from(wallet.spk_index().revealed_keychain_spks(*keychain).count())
196 .map_err(|_| Error::Wallet("Address count exceeds u64".to_string()))?;
197 total
198 .checked_add(keychain_total)
199 .ok_or_else(|| Error::Wallet("Address count exceeds u64".to_string()))
200 })?;
201
202 let items = keychains
203 .into_iter()
204 .flat_map(|(keychain, wallet_keychain)| {
205 wallet.spk_index().revealed_keychain_spks(keychain).map(
206 move |(derivation_index, script)| {
207 (keychain, wallet_keychain, derivation_index, script)
208 },
209 )
210 })
211 .skip(offset)
212 .take(limit)
213 .map(|(keychain, wallet_keychain, derivation_index, script)| {
214 let address = Address::from_script(&script, self.network)
215 .map_err(|err| Error::Wallet(err.to_string()))?;
216 let (balance_sat, confirmed_balance_sat) = balances
217 .get(&(keychain, derivation_index))
218 .copied()
219 .unwrap_or_default();
220
221 Ok(WalletAddress {
222 address: address.to_string(),
223 keychain: wallet_keychain,
224 derivation_index,
225 used: wallet.spk_index().is_used(keychain, derivation_index),
226 balance_sat,
227 confirmed_balance_sat,
228 })
229 })
230 .collect::<Result<Vec<_>, Error>>()?;
231
232 Ok(WalletPage { items, total })
233 }
234}