use std::collections::HashMap;
use bdk_wallet::bitcoin::Address;
use bdk_wallet::chain::ChainPosition;
use bdk_wallet::KeychainKind;
use crate::{CdkBdk, Error};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WalletBalance {
pub network: String,
pub synced_height: u32,
pub confirmed_sat: u64,
pub trusted_pending_sat: u64,
pub untrusted_pending_sat: u64,
pub immature_sat: u64,
pub trusted_spendable_sat: u64,
pub total_sat: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WalletTransaction {
pub txid: String,
pub received_sat: u64,
pub sent_sat: u64,
pub fee_sat: Option<u64>,
pub balance_delta_sat: i64,
pub confirmation_height: Option<u32>,
pub confirmation_time: Option<u64>,
pub first_seen: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum WalletKeychain {
External,
Internal,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WalletAddress {
pub address: String,
pub keychain: WalletKeychain,
pub derivation_index: u32,
pub used: bool,
pub balance_sat: u64,
pub confirmed_balance_sat: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WalletPage<T> {
pub items: Vec<T>,
pub total: u64,
}
impl CdkBdk {
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(),
}
}
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 })
}
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 })
}
}