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    /// Inputs spent by this transaction in input order.
38    pub inputs: Vec<WalletTransactionInput>,
39    /// Non-change payment outputs in transaction output order.
40    pub outputs: Vec<WalletTransactionOutput>,
41    /// Value received by wallet scripts in satoshis.
42    pub received_sat: u64,
43    /// Value spent from wallet inputs in satoshis.
44    pub sent_sat: u64,
45    /// Transaction fee in satoshis, when all previous outputs are known.
46    pub fee_sat: Option<u64>,
47    /// Net effect on the wallet balance in satoshis.
48    pub balance_delta_sat: i64,
49    /// Confirmation block height, when confirmed.
50    pub confirmation_height: Option<u32>,
51    /// Confirmation block timestamp, when confirmed.
52    pub confirmation_time: Option<u64>,
53    /// First-seen timestamp, when known for an unconfirmed transaction.
54    pub first_seen: Option<u64>,
55}
56
57/// An input spent by a wallet transaction.
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct WalletTransactionInput {
60    /// Transaction ID containing the previous output.
61    pub txid: String,
62    /// Index of the previous output.
63    pub vout: u32,
64    /// Value of the previous output in satoshis, when known.
65    pub amount_sat: Option<u64>,
66    /// Address of the previous output, when it belongs to the wallet.
67    pub address: Option<String>,
68}
69
70/// A payment output belonging to a wallet transaction.
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct WalletTransactionOutput {
73    /// Transaction output index.
74    pub vout: u32,
75    /// Bitcoin address in network-specific display encoding.
76    pub address: String,
77    /// Output value in satoshis.
78    pub amount_sat: u64,
79    /// Quote associated with this output, when managed by the payment backend.
80    pub quote_id: Option<String>,
81}
82
83/// BDK keychain containing a revealed address.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
85pub enum WalletKeychain {
86    /// Address intended for incoming payments.
87    External,
88    /// Internal change address.
89    Internal,
90}
91
92/// An address revealed by the BDK wallet.
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct WalletAddress {
95    /// Bitcoin address.
96    pub address: String,
97    /// Descriptor keychain.
98    pub keychain: WalletKeychain,
99    /// Child derivation index.
100    pub derivation_index: u32,
101    /// Whether the address has appeared in a wallet transaction.
102    pub used: bool,
103    /// Total current unspent balance in satoshis.
104    pub balance_sat: u64,
105    /// Confirmed current unspent balance in satoshis.
106    pub confirmed_balance_sat: u64,
107}
108
109/// A page of wallet records and the total number available.
110#[derive(Debug, Clone, PartialEq, Eq)]
111pub struct WalletPage<T> {
112    /// Records in this page.
113    pub items: Vec<T>,
114    /// Total records before pagination.
115    pub total: u64,
116}
117
118impl CdkBdk {
119    /// Creates and returns a fresh external address for operator deposits.
120    pub async fn create_operator_deposit_address(&self) -> Result<String, Error> {
121        let mut wallet_with_db = self.wallet_with_db.lock().await;
122        let address = wallet_with_db
123            .wallet
124            .reveal_next_address(KeychainKind::External)
125            .address
126            .to_string();
127
128        wallet_with_db.persist().map_err(|err| {
129            tracing::warn!("Could not persist to bdk db: {}", err);
130            Error::BdkPersist
131        })?;
132
133        Ok(address)
134    }
135
136    /// Returns the wallet balance split by confirmation status.
137    pub async fn wallet_balance(&self) -> WalletBalance {
138        let wallet_with_db = self.wallet_with_db.lock().await;
139        let balance = wallet_with_db.wallet.balance();
140
141        WalletBalance {
142            network: self.network.to_string(),
143            synced_height: wallet_with_db.wallet.latest_checkpoint().height(),
144            confirmed_sat: balance.confirmed.to_sat(),
145            trusted_pending_sat: balance.trusted_pending.to_sat(),
146            untrusted_pending_sat: balance.untrusted_pending.to_sat(),
147            immature_sat: balance.immature.to_sat(),
148            trusted_spendable_sat: balance.trusted_spendable().to_sat(),
149            total_sat: balance.total().to_sat(),
150        }
151    }
152
153    /// Returns relevant wallet transactions, newest first.
154    pub async fn wallet_transactions(
155        &self,
156        offset: usize,
157        limit: usize,
158    ) -> Result<WalletPage<WalletTransaction>, Error> {
159        self.storage.ensure_send_outpoint_quote_id_index().await?;
160        let wallet_with_db = self.wallet_with_db.lock().await;
161        let wallet = &wallet_with_db.wallet;
162        let transactions = wallet.transactions_sort_by(|left, right| {
163            right
164                .chain_position
165                .cmp(&left.chain_position)
166                .then_with(|| right.tx_node.txid.cmp(&left.tx_node.txid))
167        });
168        let total = u64::try_from(transactions.len())
169            .map_err(|_| Error::Wallet("Transaction count exceeds u64".to_string()))?;
170
171        let mut items = transactions
172            .into_iter()
173            .skip(offset)
174            .take(limit)
175            .map(|transaction| {
176                let tx = &transaction.tx_node.tx;
177                let (sent, received) = wallet.sent_and_received(tx);
178                let received_sat = received.to_sat();
179                let sent_sat = sent.to_sat();
180                let received_signed = i64::try_from(received_sat)
181                    .map_err(|_| Error::Wallet("Received value exceeds i64".to_string()))?;
182                let sent_signed = i64::try_from(sent_sat)
183                    .map_err(|_| Error::Wallet("Sent value exceeds i64".to_string()))?;
184                let is_outgoing = sent_sat > 0;
185                let inputs = tx
186                    .input
187                    .iter()
188                    .map(|input| {
189                        let outpoint = input.previous_output;
190                        let previous_output = wallet.tx_graph().get_txout(outpoint);
191                        let amount_sat = previous_output.map(|output| output.value.to_sat());
192                        let address = previous_output
193                            .filter(|output| wallet.is_mine(output.script_pubkey.clone()))
194                            .and_then(|output| {
195                                Address::from_script(output.script_pubkey.as_script(), self.network)
196                                    .ok()
197                            })
198                            .map(|address| address.to_string());
199
200                        WalletTransactionInput {
201                            txid: outpoint.txid.to_string(),
202                            vout: outpoint.vout,
203                            amount_sat,
204                            address,
205                        }
206                    })
207                    .collect();
208                let outputs = tx
209                    .output
210                    .iter()
211                    .enumerate()
212                    .filter(|(_, output)| {
213                        let is_wallet_output = wallet.is_mine(output.script_pubkey.clone());
214                        is_outgoing != is_wallet_output
215                    })
216                    .filter_map(|(vout, output)| {
217                        Address::from_script(output.script_pubkey.as_script(), self.network)
218                            .ok()
219                            .map(|address| (vout, output, address))
220                    })
221                    .map(|(vout, output, address)| {
222                        let vout = u32::try_from(vout).map_err(|_| {
223                            Error::Wallet("Transaction output index exceeds u32".to_string())
224                        })?;
225                        let address = address.to_string();
226                        Ok(WalletTransactionOutput {
227                            vout,
228                            address,
229                            amount_sat: output.value.to_sat(),
230                            quote_id: None,
231                        })
232                    })
233                    .collect::<Result<Vec<_>, Error>>()?;
234
235                let (confirmation_height, confirmation_time, first_seen) =
236                    match transaction.chain_position {
237                        ChainPosition::Confirmed { anchor, .. } => (
238                            Some(anchor.block_id.height),
239                            Some(anchor.confirmation_time),
240                            None,
241                        ),
242                        ChainPosition::Unconfirmed { first_seen, .. } => (None, None, first_seen),
243                    };
244
245                Ok(WalletTransaction {
246                    txid: transaction.tx_node.txid.to_string(),
247                    inputs,
248                    outputs,
249                    received_sat,
250                    sent_sat,
251                    fee_sat: wallet.calculate_fee(tx).ok().map(|fee| fee.to_sat()),
252                    balance_delta_sat: received_signed - sent_signed,
253                    confirmation_height,
254                    confirmation_time,
255                    first_seen,
256                })
257            })
258            .collect::<Result<Vec<_>, Error>>()?;
259        drop(wallet_with_db);
260
261        for transaction in &mut items {
262            for output in &mut transaction.outputs {
263                output.quote_id = match transaction.sent_sat > 0 {
264                    true => {
265                        let outpoint = format!("{}:{}", transaction.txid, output.vout);
266                        self.storage
267                            .get_quote_id_by_send_outpoint(&outpoint)
268                            .await?
269                    }
270                    false => {
271                        self.storage
272                            .get_quote_id_by_receive_address(&output.address)
273                            .await?
274                    }
275                };
276            }
277        }
278
279        Ok(WalletPage { items, total })
280    }
281
282    /// Returns revealed external and internal addresses in derivation order.
283    pub async fn wallet_addresses(
284        &self,
285        offset: usize,
286        limit: usize,
287    ) -> Result<WalletPage<WalletAddress>, Error> {
288        let wallet_with_db = self.wallet_with_db.lock().await;
289        let wallet = &wallet_with_db.wallet;
290        let mut balances = HashMap::<(KeychainKind, u32), (u64, u64)>::new();
291
292        for output in wallet.list_unspent() {
293            let entry = balances
294                .entry((output.keychain, output.derivation_index))
295                .or_default();
296            entry.0 = entry
297                .0
298                .checked_add(output.txout.value.to_sat())
299                .ok_or_else(|| Error::Wallet("Address balance overflow".to_string()))?;
300            if output.chain_position.is_confirmed() {
301                entry.1 = entry
302                    .1
303                    .checked_add(output.txout.value.to_sat())
304                    .ok_or_else(|| Error::Wallet("Address balance overflow".to_string()))?;
305            }
306        }
307
308        let keychains = [
309            (KeychainKind::External, WalletKeychain::External),
310            (KeychainKind::Internal, WalletKeychain::Internal),
311        ];
312        let total = keychains.iter().try_fold(0_u64, |total, (keychain, _)| {
313            let keychain_total =
314                u64::try_from(wallet.spk_index().revealed_keychain_spks(*keychain).count())
315                    .map_err(|_| Error::Wallet("Address count exceeds u64".to_string()))?;
316            total
317                .checked_add(keychain_total)
318                .ok_or_else(|| Error::Wallet("Address count exceeds u64".to_string()))
319        })?;
320
321        let items = keychains
322            .into_iter()
323            .flat_map(|(keychain, wallet_keychain)| {
324                wallet.spk_index().revealed_keychain_spks(keychain).map(
325                    move |(derivation_index, script)| {
326                        (keychain, wallet_keychain, derivation_index, script)
327                    },
328                )
329            })
330            .skip(offset)
331            .take(limit)
332            .map(|(keychain, wallet_keychain, derivation_index, script)| {
333                let address = Address::from_script(&script, self.network)
334                    .map_err(|err| Error::Wallet(err.to_string()))?;
335                let (balance_sat, confirmed_balance_sat) = balances
336                    .get(&(keychain, derivation_index))
337                    .copied()
338                    .unwrap_or_default();
339
340                Ok(WalletAddress {
341                    address: address.to_string(),
342                    keychain: wallet_keychain,
343                    derivation_index,
344                    used: wallet.spk_index().is_used(keychain, derivation_index),
345                    balance_sat,
346                    confirmed_balance_sat,
347                })
348            })
349            .collect::<Result<Vec<_>, Error>>()?;
350
351        Ok(WalletPage { items, total })
352    }
353}