cdk-bdk 0.17.5

CDK onchain backend with bdk
Documentation
//! Read-only wallet information for operator interfaces.

use std::collections::HashMap;

use bdk_wallet::bitcoin::Address;
use bdk_wallet::chain::ChainPosition;
use bdk_wallet::KeychainKind;

use crate::{CdkBdk, Error};

/// BDK wallet balance split by confirmation status.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WalletBalance {
    /// Bitcoin network used by the wallet.
    pub network: String,
    /// Height of the wallet's latest local chain checkpoint.
    pub synced_height: u32,
    /// Confirmed, spendable balance in satoshis.
    pub confirmed_sat: u64,
    /// Unconfirmed wallet-created outputs in satoshis.
    pub trusted_pending_sat: u64,
    /// Unconfirmed externally-created outputs in satoshis.
    pub untrusted_pending_sat: u64,
    /// Immature coinbase outputs in satoshis.
    pub immature_sat: u64,
    /// Confirmed plus trusted-pending balance in satoshis.
    pub trusted_spendable_sat: u64,
    /// Total wallet balance in satoshis.
    pub total_sat: u64,
}

/// A transaction relevant to the BDK wallet.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WalletTransaction {
    /// Transaction ID.
    pub txid: String,
    /// Value received by wallet scripts in satoshis.
    pub received_sat: u64,
    /// Value spent from wallet inputs in satoshis.
    pub sent_sat: u64,
    /// Transaction fee in satoshis, when all previous outputs are known.
    pub fee_sat: Option<u64>,
    /// Net effect on the wallet balance in satoshis.
    pub balance_delta_sat: i64,
    /// Confirmation block height, when confirmed.
    pub confirmation_height: Option<u32>,
    /// Confirmation block timestamp, when confirmed.
    pub confirmation_time: Option<u64>,
    /// First-seen timestamp, when known for an unconfirmed transaction.
    pub first_seen: Option<u64>,
}

/// BDK keychain containing a revealed address.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum WalletKeychain {
    /// Address intended for incoming payments.
    External,
    /// Internal change address.
    Internal,
}

/// An address revealed by the BDK wallet.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WalletAddress {
    /// Bitcoin address.
    pub address: String,
    /// Descriptor keychain.
    pub keychain: WalletKeychain,
    /// Child derivation index.
    pub derivation_index: u32,
    /// Whether the address has appeared in a wallet transaction.
    pub used: bool,
    /// Total current unspent balance in satoshis.
    pub balance_sat: u64,
    /// Confirmed current unspent balance in satoshis.
    pub confirmed_balance_sat: u64,
}

/// A page of wallet records and the total number available.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WalletPage<T> {
    /// Records in this page.
    pub items: Vec<T>,
    /// Total records before pagination.
    pub total: u64,
}

impl CdkBdk {
    /// Returns the wallet balance split by confirmation status.
    pub async fn wallet_balance(&self) -> WalletBalance {
        let wallet_with_db = self.wallet_with_db.lock().await;
        let balance = wallet_with_db.wallet.balance();

        WalletBalance {
            network: self.network.to_string(),
            synced_height: wallet_with_db.wallet.latest_checkpoint().height(),
            confirmed_sat: balance.confirmed.to_sat(),
            trusted_pending_sat: balance.trusted_pending.to_sat(),
            untrusted_pending_sat: balance.untrusted_pending.to_sat(),
            immature_sat: balance.immature.to_sat(),
            trusted_spendable_sat: balance.trusted_spendable().to_sat(),
            total_sat: balance.total().to_sat(),
        }
    }

    /// Returns relevant wallet transactions, newest first.
    pub async fn wallet_transactions(
        &self,
        offset: usize,
        limit: usize,
    ) -> Result<WalletPage<WalletTransaction>, Error> {
        let wallet_with_db = self.wallet_with_db.lock().await;
        let wallet = &wallet_with_db.wallet;
        let transactions = wallet.transactions_sort_by(|left, right| {
            right
                .chain_position
                .cmp(&left.chain_position)
                .then_with(|| right.tx_node.txid.cmp(&left.tx_node.txid))
        });
        let total = u64::try_from(transactions.len())
            .map_err(|_| Error::Wallet("Transaction count exceeds u64".to_string()))?;

        let items = transactions
            .into_iter()
            .skip(offset)
            .take(limit)
            .map(|transaction| {
                let tx = &transaction.tx_node.tx;
                let (sent, received) = wallet.sent_and_received(tx);
                let received_sat = received.to_sat();
                let sent_sat = sent.to_sat();
                let received_signed = i64::try_from(received_sat)
                    .map_err(|_| Error::Wallet("Received value exceeds i64".to_string()))?;
                let sent_signed = i64::try_from(sent_sat)
                    .map_err(|_| Error::Wallet("Sent value exceeds i64".to_string()))?;

                let (confirmation_height, confirmation_time, first_seen) =
                    match transaction.chain_position {
                        ChainPosition::Confirmed { anchor, .. } => (
                            Some(anchor.block_id.height),
                            Some(anchor.confirmation_time),
                            None,
                        ),
                        ChainPosition::Unconfirmed { first_seen, .. } => (None, None, first_seen),
                    };

                Ok(WalletTransaction {
                    txid: transaction.tx_node.txid.to_string(),
                    received_sat,
                    sent_sat,
                    fee_sat: wallet.calculate_fee(tx).ok().map(|fee| fee.to_sat()),
                    balance_delta_sat: received_signed - sent_signed,
                    confirmation_height,
                    confirmation_time,
                    first_seen,
                })
            })
            .collect::<Result<Vec<_>, Error>>()?;

        Ok(WalletPage { items, total })
    }

    /// Returns revealed external and internal addresses in derivation order.
    pub async fn wallet_addresses(
        &self,
        offset: usize,
        limit: usize,
    ) -> Result<WalletPage<WalletAddress>, Error> {
        let wallet_with_db = self.wallet_with_db.lock().await;
        let wallet = &wallet_with_db.wallet;
        let mut balances = HashMap::<(KeychainKind, u32), (u64, u64)>::new();

        for output in wallet.list_unspent() {
            let entry = balances
                .entry((output.keychain, output.derivation_index))
                .or_default();
            entry.0 = entry
                .0
                .checked_add(output.txout.value.to_sat())
                .ok_or_else(|| Error::Wallet("Address balance overflow".to_string()))?;
            if output.chain_position.is_confirmed() {
                entry.1 = entry
                    .1
                    .checked_add(output.txout.value.to_sat())
                    .ok_or_else(|| Error::Wallet("Address balance overflow".to_string()))?;
            }
        }

        let keychains = [
            (KeychainKind::External, WalletKeychain::External),
            (KeychainKind::Internal, WalletKeychain::Internal),
        ];
        let total = keychains.iter().try_fold(0_u64, |total, (keychain, _)| {
            let keychain_total =
                u64::try_from(wallet.spk_index().revealed_keychain_spks(*keychain).count())
                    .map_err(|_| Error::Wallet("Address count exceeds u64".to_string()))?;
            total
                .checked_add(keychain_total)
                .ok_or_else(|| Error::Wallet("Address count exceeds u64".to_string()))
        })?;

        let items = keychains
            .into_iter()
            .flat_map(|(keychain, wallet_keychain)| {
                wallet.spk_index().revealed_keychain_spks(keychain).map(
                    move |(derivation_index, script)| {
                        (keychain, wallet_keychain, derivation_index, script)
                    },
                )
            })
            .skip(offset)
            .take(limit)
            .map(|(keychain, wallet_keychain, derivation_index, script)| {
                let address = Address::from_script(&script, self.network)
                    .map_err(|err| Error::Wallet(err.to_string()))?;
                let (balance_sat, confirmed_balance_sat) = balances
                    .get(&(keychain, derivation_index))
                    .copied()
                    .unwrap_or_default();

                Ok(WalletAddress {
                    address: address.to_string(),
                    keychain: wallet_keychain,
                    derivation_index,
                    used: wallet.spk_index().is_used(keychain, derivation_index),
                    balance_sat,
                    confirmed_balance_sat,
                })
            })
            .collect::<Result<Vec<_>, Error>>()?;

        Ok(WalletPage { items, total })
    }
}