From 54410cd9c435e1e6228b985d78e55b357c2368b8 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Thu, 8 Dec 2022 16:05:47 +0100 Subject: [PATCH] database: allow to query coins by their spending status --- src/bitcoin/poller/looper.rs | 4 ++-- src/commands/mod.rs | 4 ++-- src/database/mod.rs | 13 ++++++++--- src/database/sqlite/mod.rs | 43 +++++++++++++++++++++++++++--------- src/testutils.rs | 17 +++++++++++--- 5 files changed, 61 insertions(+), 20 deletions(-) diff --git a/src/bitcoin/poller/looper.rs b/src/bitcoin/poller/looper.rs index aa9c8648..807965b3 100644 --- a/src/bitcoin/poller/looper.rs +++ b/src/bitcoin/poller/looper.rs @@ -1,6 +1,6 @@ use crate::{ bitcoin::{BitcoinInterface, BlockChainTip, UTxO}, - database::{Coin, DatabaseConnection, DatabaseInterface}, + database::{Coin, CoinType, DatabaseConnection, DatabaseInterface}, descriptors, }; @@ -30,7 +30,7 @@ fn update_coins( descs: &[descriptors::InheritanceDescriptor], secp: &secp256k1::Secp256k1, ) -> UpdatedCoins { - let curr_coins = db_conn.coins(); + let curr_coins = db_conn.coins(CoinType::All); log::debug!("Current coins: {:?}", curr_coins); // Start by fetching newly received coins. diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 89247970..2c5f1dd2 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -6,7 +6,7 @@ mod utils; use crate::{ bitcoin::BitcoinInterface, - database::{Coin, DatabaseInterface}, + database::{Coin, CoinType, DatabaseInterface}, descriptors, DaemonControl, VERSION, }; @@ -243,7 +243,7 @@ impl DaemonControl { pub fn list_coins(&self) -> ListCoinsResult { let mut db_conn = self.db.connection(); let coins: Vec = db_conn - .coins() + .coins(CoinType::All) // Can't use into_values as of Rust 1.48 .into_iter() .map(|(_, coin)| { diff --git a/src/database/mod.rs b/src/database/mod.rs index 494c6cee..6662a365 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -81,7 +81,7 @@ pub trait DatabaseConnection { ) -> Option<(bip32::ChildNumber, bool)>; /// Get all our coins, past or present, spent or not. - fn coins(&mut self) -> HashMap; + fn coins(&mut self, coin_type: CoinType) -> HashMap; /// List coins that are being spent and whose spending transaction is still unconfirmed. fn list_spending_coins(&mut self) -> HashMap; @@ -178,8 +178,8 @@ impl DatabaseConnection for SqliteConn { self.complete_wallet_rescan() } - fn coins(&mut self) -> HashMap { - self.coins() + fn coins(&mut self, coin_type: CoinType) -> HashMap { + self.coins(coin_type) .into_iter() .map(|db_coin| (db_coin.outpoint, db_coin.into())) .collect() @@ -316,3 +316,10 @@ impl Coin { self.spend_txid.is_some() } } + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum CoinType { + All, + Unspent, + Spent, +} diff --git a/src/database/sqlite/mod.rs b/src/database/sqlite/mod.rs index d5233b69..9f8ca382 100644 --- a/src/database/sqlite/mod.rs +++ b/src/database/sqlite/mod.rs @@ -16,7 +16,7 @@ use crate::{ schema::{DbAddress, DbCoin, DbSpendTransaction, DbTip, DbWallet}, utils::{create_fresh_db, db_exec, db_query, db_tx_query, LOOK_AHEAD_LIMIT}, }, - Coin, + Coin, CoinType, }, descriptors::MultipathDescriptor, }; @@ -313,10 +313,14 @@ impl SqliteConn { } /// Get all the coins from DB. - pub fn coins(&mut self) -> Vec { + pub fn coins(&mut self, coin_type: CoinType) -> Vec { db_query( &mut self.conn, - "SELECT * FROM coins", + match coin_type { + CoinType::All => "SELECT * FROM coins", + CoinType::Unspent => "SELECT * FROM coins WHERE spend_txid IS NULL", + CoinType::Spent => "SELECT * FROM coins WHERE spend_txid IS NOT NULL", + }, rusqlite::params![], |row| row.try_into(), ) @@ -682,7 +686,7 @@ mod tests { let mut conn = db.connection().unwrap(); // Necessarily empty at first. - assert!(conn.coins().is_empty()); + assert!(conn.coins(CoinType::All).is_empty()); // Add one, we'll get it. let coin_a = Coin { @@ -699,13 +703,17 @@ mod tests { spend_block: None, }; conn.new_unspent_coins(&[coin_a]); - assert_eq!(conn.coins()[0].outpoint, coin_a.outpoint); + assert_eq!(conn.coins(CoinType::All)[0].outpoint, coin_a.outpoint); // We can query it by its outpoint let coins = conn.db_coins(&[coin_a.outpoint]); assert_eq!(coins.len(), 1); assert_eq!(coins[0].outpoint, coin_a.outpoint); + // It is unspent. + assert_eq!(conn.coins(CoinType::Unspent)[0].outpoint, coin_a.outpoint); + assert!(conn.coins(CoinType::Spent).is_empty()); + // Add a second one (this one is change), we'll get both. let coin_b = Coin { outpoint: bitcoin::OutPoint::from_str( @@ -721,8 +729,11 @@ mod tests { spend_block: None, }; conn.new_unspent_coins(&[coin_b]); - let outpoints: HashSet = - conn.coins().into_iter().map(|c| c.outpoint).collect(); + let outpoints: HashSet = conn + .coins(CoinType::All) + .into_iter() + .map(|c| c.outpoint) + .collect(); assert!(outpoints.contains(&coin_a.outpoint)); assert!(outpoints.contains(&coin_b.outpoint)); @@ -738,11 +749,15 @@ mod tests { assert!(coins.iter().any(|c| c.outpoint == coin_a.outpoint)); assert!(coins.iter().any(|c| c.outpoint == coin_b.outpoint)); + // They are both unspent + assert_eq!(conn.coins(CoinType::Unspent).len(), 2); + assert!(conn.coins(CoinType::Spent).is_empty()); + // Now if we confirm one, it'll be marked as such. let height = 174500; let time = 174500; conn.confirm_coins(&[(coin_a.outpoint, height, time)]); - let coins = conn.coins(); + let coins = conn.coins(CoinType::All); assert_eq!(coins[0].block_height, Some(height)); assert_eq!(coins[0].block_time, Some(time)); assert!(coins[1].block_height.is_none()); @@ -753,14 +768,18 @@ mod tests { coin_a.outpoint, bitcoin::Txid::from_slice(&[0; 32][..]).unwrap(), )]); - let coins_map: HashMap = - conn.coins().into_iter().map(|c| (c.outpoint, c)).collect(); + let coins_map: HashMap = conn + .coins(CoinType::All) + .into_iter() + .map(|c| (c.outpoint, c)) + .collect(); assert!(coins_map .get(&coin_a.outpoint) .unwrap() .spend_txid .is_some()); + // We will see it as 'spending' let outpoints: HashSet = conn .list_spending_coins() .into_iter() @@ -768,6 +787,10 @@ mod tests { .collect(); assert!(outpoints.contains(&coin_a.outpoint)); + // The first one is spent, not the second one. + assert_eq!(conn.coins(CoinType::Spent)[0].outpoint, coin_a.outpoint); + assert_eq!(conn.coins(CoinType::Unspent)[0].outpoint, coin_b.outpoint); + // Now if we confirm the spend. let height = 128_097; let time = 3_000_000; diff --git a/src/testutils.rs b/src/testutils.rs index c42a743a..e50eafef 100644 --- a/src/testutils.rs +++ b/src/testutils.rs @@ -1,7 +1,7 @@ use crate::{ bitcoin::{BitcoinInterface, Block, BlockChainTip, UTxO}, config::{BitcoinConfig, Config}, - database::{Coin, DatabaseConnection, DatabaseInterface, SpendBlock}, + database::{Coin, CoinType, DatabaseConnection, DatabaseInterface, SpendBlock}, descriptors, DaemonHandle, }; @@ -189,8 +189,19 @@ impl DatabaseConnection for DummyDatabase { self.db.write().unwrap().change_index = index; } - fn coins(&mut self) -> HashMap { - self.db.read().unwrap().coins.clone() + fn coins(&mut self, coin_type: CoinType) -> HashMap { + let coins = self.db.read().unwrap().coins.clone(); + match coin_type { + CoinType::All => coins, + CoinType::Unspent => coins + .into_iter() + .filter(|(_, c)| c.spend_txid.is_none()) + .collect(), + CoinType::Spent => coins + .into_iter() + .filter(|(_, c)| c.spend_txid.is_some()) + .collect(), + } } fn list_spending_coins(&mut self) -> HashMap {