//! Database interface for Liana. //! //! Record wallet metadata, spent and unspent coins, ongoing transactions. pub mod sqlite; use crate::{ bitcoin::BlockChainTip, database::sqlite::{ schema::{DbBlockInfo, DbCoin, DbTip}, SqliteConn, SqliteDb, }, }; use std::{ collections::{HashMap, HashSet}, fmt::Display, iter::FromIterator, str::FromStr, sync, }; use miniscript::bitcoin::{self, bip32, psbt::Psbt, secp256k1}; /// Information about the wallet. /// /// All timestamps are the number of seconds since the UNIX epoch. #[derive(Clone, Debug)] pub struct Wallet { /// Timestamp at wallet creation time. pub timestamp: u32, /// Derivation index for the next receiving address. pub receive_index: bip32::ChildNumber, /// Derivation index for the next change address. pub change_index: bip32::ChildNumber, /// Timestamp to start rescanning from, if any. pub rescan_timestamp: Option, /// Timestamp at which the last poll of the blockchain completed, if any, pub last_poll_timestamp: Option, } pub trait DatabaseInterface: Send { fn connection(&self) -> Box; } impl DatabaseInterface for SqliteDb { fn connection(&self) -> Box { Box::new(self.connection().expect("Database must be available")) } } // FIXME: do we need to repeat the entire trait implemenation? Isn't there a nicer way? impl DatabaseInterface for sync::Arc> { fn connection(&self) -> Box { self.lock().unwrap().connection() } } pub trait DatabaseConnection { /// Get the tip of the best chain we've seen. fn chain_tip(&mut self) -> Option; /// The network we are operating on. fn network(&mut self) -> bitcoin::Network; /// Get the `Wallet`. fn wallet(&mut self) -> Wallet; /// The timestamp at wallet creation time fn timestamp(&mut self) -> u32; /// Update our best chain seen. fn update_tip(&mut self, tip: &BlockChainTip); /// Get the derivation index for the next receiving address fn receive_index(&mut self) -> bip32::ChildNumber; /// Set the derivation index for the next receiving address fn set_receive_index( &mut self, index: bip32::ChildNumber, secp: &secp256k1::Secp256k1, ); /// Get the derivation index for the next change address fn change_index(&mut self) -> bip32::ChildNumber; /// Set the derivation index for the next change address fn set_change_index( &mut self, index: bip32::ChildNumber, secp: &secp256k1::Secp256k1, ); /// Get the timestamp at which to start rescaning from, if any. fn rescan_timestamp(&mut self) -> Option; /// Set a timestamp at which to start rescaning the block chain from. fn set_rescan(&mut self, timestamp: u32); /// Mark the rescan as complete. fn complete_rescan(&mut self); /// Get the timestamp at which the last poll of the blockchain completed, if any, /// as the number of seconds since the UNIX epoch. fn last_poll_timestamp(&mut self) -> Option; /// Set the timestamp at which the last poll of the blockchain completed, /// where `timestamp` should be given as the number of seconds since the UNIX epoch. fn set_last_poll(&mut self, timestamp: u32); /// Get the derivation index for this address, as well as whether this address is change. fn derivation_index_by_address( &mut self, address: &bitcoin::Address, ) -> Option<(bip32::ChildNumber, bool)>; /// Get all our coins, past or present, spent or not. fn coins( &mut self, statuses: &[CoinStatus], outpoints: &[bitcoin::OutPoint], ) -> HashMap; /// List coins that are being spent and whose spending transaction is still unconfirmed. fn list_spending_coins(&mut self) -> HashMap; /// Store new UTxOs. Coins must not already be in database. fn new_unspent_coins(&mut self, coins: &[Coin]); /// Remove some UTxOs from the database. fn remove_coins(&mut self, coins: &[bitcoin::OutPoint]); /// Mark a set of coins as being confirmed at a specified height and block time. /// NOTE: if the coin comes from an immature coinbase transaction, this will mark it as mature. /// Immature coinbase deposits must not be confirmed before they are 100 blocks deep in the /// chain. fn confirm_coins(&mut self, outpoints: &[(bitcoin::OutPoint, i32, u32)]); /// Mark a set of coins as being spent by a specified txid of a pending transaction. fn spend_coins(&mut self, outpoints: &[(bitcoin::OutPoint, bitcoin::Txid)]); /// Mark a set of coins as not being spent anymore. fn unspend_coins(&mut self, outpoints: &[bitcoin::OutPoint]); /// Mark a set of coins as spent by a specified txid at a specified block time. fn confirm_spend(&mut self, outpoints: &[(bitcoin::OutPoint, bitcoin::Txid, i32, u32)]); /// Get specific coins from the database. fn coins_by_outpoints( &mut self, outpoints: &[bitcoin::OutPoint], ) -> HashMap; fn spend_tx(&mut self, txid: &bitcoin::Txid) -> Option; /// Insert a new Spend transaction or replace an existing one. fn store_spend(&mut self, psbt: &Psbt); /// List all existing Spend transactions, along with an optional last update timestamp. fn list_spend(&mut self) -> Vec<(Psbt, Option)>; /// Delete a Spend transaction from database. fn delete_spend(&mut self, txid: &bitcoin::Txid); /// Update, for a set of items (as key), their label (as value). A `None` value deletes the /// label. fn update_labels(&mut self, items: &HashMap>); fn labels(&mut self, labels: &HashSet) -> HashMap; /// Mark the given tip as the new best seen block. Update stored data accordingly. fn rollback_tip(&mut self, new_tip: &BlockChainTip); /// Retrieve a limited list of txids that where deposited or spent between the start and end timestamps (inclusive bounds) fn list_txids(&mut self, start: u32, end: u32, limit: u64) -> Vec; /// Retrieves all txids from the transactions table whether or not they are referenced by a coin. fn list_saved_txids(&mut self) -> Vec; /// Store transactions in database, ignoring any that already exist. fn new_txs(&mut self, txs: &[bitcoin::Transaction]); /// Retrieve a list of transactions and their corresponding block heights and times. fn list_wallet_transactions( &mut self, txids: &[bitcoin::Txid], ) -> Vec<(bitcoin::Transaction, Option, Option)>; } impl DatabaseConnection for SqliteConn { fn chain_tip(&mut self) -> Option { match self.db_tip() { DbTip { block_height: Some(height), block_hash: Some(hash), .. } => Some(BlockChainTip { height, hash }), _ => None, } } fn network(&mut self) -> bitcoin::Network { self.db_tip().network } fn wallet(&mut self) -> Wallet { let db_wallet = self.db_wallet(); Wallet { timestamp: db_wallet.timestamp, receive_index: db_wallet.deposit_derivation_index, change_index: db_wallet.change_derivation_index, rescan_timestamp: db_wallet.rescan_timestamp, last_poll_timestamp: db_wallet.last_poll_timestamp, } } fn timestamp(&mut self) -> u32 { self.wallet().timestamp } fn update_tip(&mut self, tip: &BlockChainTip) { self.update_tip(tip) } fn receive_index(&mut self) -> bip32::ChildNumber { self.wallet().receive_index } fn set_receive_index( &mut self, index: bip32::ChildNumber, secp: &secp256k1::Secp256k1, ) { self.set_derivation_index(index, false, secp) } fn change_index(&mut self) -> bip32::ChildNumber { self.wallet().change_index } fn set_change_index( &mut self, index: bip32::ChildNumber, secp: &secp256k1::Secp256k1, ) { self.set_derivation_index(index, true, secp) } fn rescan_timestamp(&mut self) -> Option { self.wallet().rescan_timestamp } fn set_rescan(&mut self, timestamp: u32) { self.set_wallet_rescan_timestamp(timestamp) } fn complete_rescan(&mut self) { self.complete_wallet_rescan() } fn last_poll_timestamp(&mut self) -> Option { self.wallet().last_poll_timestamp } fn set_last_poll(&mut self, timestamp: u32) { self.set_wallet_last_poll_timestamp(timestamp) .expect("database must be available") } fn coins( &mut self, statuses: &[CoinStatus], outpoints: &[bitcoin::OutPoint], ) -> HashMap { self.coins(statuses, outpoints) .into_iter() .map(|db_coin| (db_coin.outpoint, db_coin.into())) .collect() } fn list_spending_coins(&mut self) -> HashMap { self.list_spending_coins() .into_iter() .map(|db_coin| (db_coin.outpoint, db_coin.into())) .collect() } fn new_unspent_coins<'a>(&mut self, coins: &[Coin]) { self.new_unspent_coins(coins) } fn remove_coins(&mut self, outpoints: &[bitcoin::OutPoint]) { self.remove_coins(outpoints) } fn confirm_coins<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, i32, u32)]) { self.confirm_coins(outpoints) } fn spend_coins<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, bitcoin::Txid)]) { self.spend_coins(outpoints) } fn unspend_coins(&mut self, outpoints: &[bitcoin::OutPoint]) { self.unspend_coins(outpoints) } fn confirm_spend<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, bitcoin::Txid, i32, u32)]) { self.confirm_spend(outpoints) } fn derivation_index_by_address( &mut self, address: &bitcoin::Address, ) -> Option<(bip32::ChildNumber, bool)> { self.db_address(address).map(|db_addr| { ( db_addr.derivation_index, // We only compare address strings in case `assume_checked()` uses a different network. // E.g. An unchecked signet address would have its network set to testnet and so comparing // to a signet `Address` would never match. address.to_string() == db_addr.change_address.assume_checked().to_string(), ) }) } fn coins_by_outpoints( &mut self, outpoints: &[bitcoin::OutPoint], ) -> HashMap { self.db_coins(outpoints) .into_iter() .map(|db_coin| (db_coin.outpoint, db_coin.into())) .collect() } fn spend_tx(&mut self, txid: &bitcoin::Txid) -> Option { self.db_spend(txid).map(|db_spend| db_spend.psbt) } fn store_spend(&mut self, psbt: &Psbt) { self.store_spend(psbt) } fn list_spend(&mut self) -> Vec<(Psbt, Option)> { self.list_spend() .into_iter() .map(|db_spend| (db_spend.psbt, db_spend.updated_at)) .collect() } fn delete_spend(&mut self, txid: &bitcoin::Txid) { self.delete_spend(txid) } fn update_labels(&mut self, items: &HashMap>) { self.update_labels(items) } fn labels(&mut self, items: &HashSet) -> HashMap { let labels = self.db_labels(items); HashMap::from_iter(labels.into_iter().map(|label| (label.item, label.value))) } fn rollback_tip(&mut self, new_tip: &BlockChainTip) { self.rollback_tip(new_tip) } fn list_txids(&mut self, start: u32, end: u32, limit: u64) -> Vec { self.db_list_txids(start, end, limit) } fn list_saved_txids(&mut self) -> Vec { self.db_list_saved_txids() } fn new_txs<'a>(&mut self, txs: &[bitcoin::Transaction]) { self.new_txs(txs) } fn list_wallet_transactions( &mut self, txids: &[bitcoin::Txid], ) -> Vec<(bitcoin::Transaction, Option, Option)> { self.list_wallet_transactions(txids) .into_iter() .map(|wtx| { ( wtx.transaction, wtx.block_info.map(|b| b.height), wtx.block_info.map(|b| b.time), ) }) .collect() } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct BlockInfo { pub height: i32, pub time: u32, } impl From for BlockInfo { fn from(b: DbBlockInfo) -> BlockInfo { BlockInfo { height: b.height, time: b.time, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Coin { pub outpoint: bitcoin::OutPoint, pub is_immature: bool, pub block_info: Option, pub amount: bitcoin::Amount, pub derivation_index: bip32::ChildNumber, pub is_change: bool, pub spend_txid: Option, pub spend_block: Option, } impl std::convert::From for Coin { fn from(db_coin: DbCoin) -> Coin { let DbCoin { outpoint, is_immature, block_info, amount, derivation_index, is_change, spend_txid, spend_block, .. } = db_coin; Coin { outpoint, is_immature, block_info: block_info.map(BlockInfo::from), amount, derivation_index, is_change, spend_txid, spend_block: spend_block.map(BlockInfo::from), } } } impl Coin { pub fn is_confirmed(&self) -> bool { self.block_info.is_some() } pub fn is_spent(&self) -> bool { self.spend_txid.is_some() } } /// Possible (mutually exclusive) status of a coin. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum CoinStatus { /// Has not yet been included in a block and has no spend transaction. Unconfirmed, /// Has been included in a block and has no spend transaction. Confirmed, /// Has an unconfirmed spend transaction, but coin itself may not yet have been included in a block. Spending, /// Has a confirmed spend transaction. Spent, } impl CoinStatus { pub fn from_arg(s: &str) -> Option { match s { "unconfirmed" => Some(CoinStatus::Unconfirmed), "confirmed" => Some(CoinStatus::Confirmed), "spending" => Some(CoinStatus::Spending), "spent" => Some(CoinStatus::Spent), _ => None, } } /// Converts a `CoinStatus` to its equivalent argument name /// as used in the `listcoins` RPC command. pub fn to_arg(&self) -> &'static str { match self { CoinStatus::Unconfirmed => "unconfirmed", CoinStatus::Confirmed => "confirmed", CoinStatus::Spending => "spending", CoinStatus::Spent => "spent", } } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum LabelItem { Address(bitcoin::Address), Txid(bitcoin::Txid), OutPoint(bitcoin::OutPoint), } impl From for LabelItem { fn from(value: bitcoin::Address) -> Self { Self::Address(value) } } impl From for LabelItem { fn from(value: bitcoin::Txid) -> Self { Self::Txid(value) } } impl From for LabelItem { fn from(value: bitcoin::OutPoint) -> Self { Self::OutPoint(value) } } impl Display for LabelItem { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { LabelItem::Address(a) => write!(f, "{}", a), LabelItem::Txid(a) => write!(f, "{}", a), LabelItem::OutPoint(a) => write!(f, "{}", a), } } } impl LabelItem { pub fn from_str(s: &str, network: bitcoin::Network) -> Option { if let Ok(addr) = bitcoin::Address::from_str(s) { if !addr.is_valid_for_network(network) { None } else { Some(LabelItem::Address(addr.assume_checked())) } } else if let Ok(txid) = bitcoin::Txid::from_str(s) { Some(LabelItem::Txid(txid)) } else if let Ok(outpoint) = bitcoin::OutPoint::from_str(s) { Some(LabelItem::OutPoint(outpoint)) } else { None } } } #[cfg(test)] mod tests { use super::*; #[test] fn coin_status_as_arg() { assert_eq!( CoinStatus::from_arg(CoinStatus::Unconfirmed.to_arg()), Some(CoinStatus::Unconfirmed) ); assert_eq!( CoinStatus::from_arg(CoinStatus::Confirmed.to_arg()), Some(CoinStatus::Confirmed) ); assert_eq!( CoinStatus::from_arg(CoinStatus::Spending.to_arg()), Some(CoinStatus::Spending) ); assert_eq!( CoinStatus::from_arg(CoinStatus::Spent.to_arg()), Some(CoinStatus::Spent) ); } }