Skip to main content

cdk_bdk/
wallet_info.rs

1//! Read-only wallet information for operator interfaces.
2
3use 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/// BDK wallet balance split by confirmation status.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct WalletBalance {
14    /// Bitcoin network used by the wallet.
15    pub network: String,
16    /// Height of the wallet's latest local chain checkpoint.
17    pub synced_height: u32,
18    /// Confirmed, spendable balance in satoshis.
19    pub confirmed_sat: u64,
20    /// Unconfirmed wallet-created outputs in satoshis.
21    pub trusted_pending_sat: u64,
22    /// Unconfirmed externally-created outputs in satoshis.
23    pub untrusted_pending_sat: u64,
24    /// Immature coinbase outputs in satoshis.
25    pub immature_sat: u64,
26    /// Confirmed plus trusted-pending balance in satoshis.
27    pub trusted_spendable_sat: u64,
28    /// Total wallet balance in satoshis.
29    pub total_sat: u64,
30}
31
32/// A transaction relevant to the BDK wallet.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct WalletTransaction {
35    /// Transaction ID.
36    pub txid: String,
37    /// Value received by wallet scripts in satoshis.
38    pub received_sat: u64,
39    /// Value spent from wallet inputs in satoshis.
40    pub sent_sat: u64,
41    /// Transaction fee in satoshis, when all previous outputs are known.
42    pub fee_sat: Option<u64>,
43    /// Net effect on the wallet balance in satoshis.
44    pub balance_delta_sat: i64,
45    /// Confirmation block height, when confirmed.
46    pub confirmation_height: Option<u32>,
47    /// Confirmation block timestamp, when confirmed.
48    pub confirmation_time: Option<u64>,
49    /// First-seen timestamp, when known for an unconfirmed transaction.
50    pub first_seen: Option<u64>,
51}
52
53/// BDK keychain containing a revealed address.
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
55pub enum WalletKeychain {
56    /// Address intended for incoming payments.
57    External,
58    /// Internal change address.
59    Internal,
60}
61
62/// An address revealed by the BDK wallet.
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct WalletAddress {
65    /// Bitcoin address.
66    pub address: String,
67    /// Descriptor keychain.
68    pub keychain: WalletKeychain,
69    /// Child derivation index.
70    pub derivation_index: u32,
71    /// Whether the address has appeared in a wallet transaction.
72    pub used: bool,
73    /// Total current unspent balance in satoshis.
74    pub balance_sat: u64,
75    /// Confirmed current unspent balance in satoshis.
76    pub confirmed_balance_sat: u64,
77}
78
79/// A page of wallet records and the total number available.
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct WalletPage<T> {
82    /// Records in this page.
83    pub items: Vec<T>,
84    /// Total records before pagination.
85    pub total: u64,
86}
87
88impl CdkBdk {
89    /// Returns the wallet balance split by confirmation status.
90    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    /// Returns relevant wallet transactions, newest first.
107    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    /// Returns revealed external and internal addresses in derivation order.
164    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}