diff --git a/src/database/mod.rs b/src/database/mod.rs index 68f5d105..4a7afc30 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -62,6 +62,39 @@ pub trait DatabaseConnection { /// Mark a set of coins as being spent by a specified txid. fn spend_coins<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, bitcoin::Txid)]); + + /// Get specific coins from the database. + fn coins_by_outpoints( + &mut self, + outpoints: &[bitcoin::OutPoint], + ) -> HashMap; +} + +// FIXME: if possible, avoid reallocating. +fn db_coins_into_coins(db_coins: Vec) -> HashMap { + db_coins + .into_iter() + .map(|db_coin| { + let DbCoin { + outpoint, + block_height, + amount, + derivation_index, + spend_txid, + .. + } = db_coin; + ( + outpoint, + Coin { + outpoint, + block_height, + amount, + derivation_index, + spend_txid, + }, + ) + }) + .collect() } impl DatabaseConnection for SqliteConn { @@ -93,30 +126,7 @@ impl DatabaseConnection for SqliteConn { } fn unspent_coins(&mut self) -> HashMap { - // FIXME: if possible, avoid reallocating. - self.unspent_coins() - .into_iter() - .map(|db_coin| { - let DbCoin { - outpoint, - block_height, - amount, - derivation_index, - spend_txid, - .. - } = db_coin; - ( - outpoint, - Coin { - outpoint, - block_height, - amount, - derivation_index, - spend_txid, - }, - ) - }) - .collect() + db_coins_into_coins(self.unspent_coins()) } fn new_unspent_coins<'a>(&mut self, coins: &[Coin]) { @@ -138,6 +148,13 @@ impl DatabaseConnection for SqliteConn { self.db_address(address) .map(|db_addr| db_addr.derivation_index) } + + fn coins_by_outpoints( + &mut self, + outpoints: &[bitcoin::OutPoint], + ) -> HashMap { + db_coins_into_coins(self.db_coins(outpoints)) + } } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/src/database/sqlite/mod.rs b/src/database/sqlite/mod.rs index 8406ff37..373de446 100644 --- a/src/database/sqlite/mod.rs +++ b/src/database/sqlite/mod.rs @@ -23,7 +23,7 @@ use crate::{ use std::{convert::TryInto, fmt, io, path}; -use miniscript::bitcoin::{self, secp256k1}; +use miniscript::bitcoin::{self, hashes::hex::ToHex, secp256k1}; const DB_VERSION: i64 = 0; @@ -330,6 +330,28 @@ impl SqliteConn { .expect("Db must not fail") .pop() } + + pub fn db_coins(&mut self, outpoints: &[bitcoin::OutPoint]) -> Vec { + // SELECT * FROM coins WHERE (txid, vout) IN ((txidA, voutA), (txidB, voutB)); + let mut query = "SELECT * FROM coins WHERE (txid, vout) IN (VALUES ".to_string(); + for (i, outpoint) in outpoints.iter().enumerate() { + // NOTE: the txid is not stored as little-endian. Convert it to vec first. + query += &format!( + "(x'{}', {})", + &outpoint.txid.to_vec().to_hex(), + outpoint.vout + ); + if i != outpoints.len() - 1 { + query += ", "; + } + } + query += ")"; + + db_query(&mut self.conn, &query, rusqlite::params![], |row| { + row.try_into() + }) + .expect("Db must not fail") + } } #[cfg(test)] @@ -462,6 +484,11 @@ mod tests { conn.new_unspent_coins(&[coin_a.clone()]); // On 1.48, arrays aren't IntoIterator assert_eq!(conn.unspent_coins()[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); + // Add a second one, we'll get both. let coin_b = Coin { outpoint: bitcoin::OutPoint::from_str( @@ -482,6 +509,24 @@ mod tests { assert!(outpoints.contains(&coin_a.outpoint)); assert!(outpoints.contains(&coin_b.outpoint)); + // We can query both by their outpoints + let coins = conn.db_coins(&[coin_a.outpoint]); + assert_eq!(coins.len(), 1); + assert_eq!(coins[0].outpoint, coin_a.outpoint); + let coins = conn.db_coins(&[coin_b.outpoint]); + assert_eq!(coins.len(), 1); + assert_eq!(coins[0].outpoint, coin_b.outpoint); + let coins = conn.db_coins(&[coin_a.outpoint, coin_b.outpoint]); + assert_eq!(coins.len(), 2); + assert!(coins + .iter() + .find(|c| c.outpoint == coin_a.outpoint) + .is_some()); + assert!(coins + .iter() + .find(|c| c.outpoint == coin_b.outpoint) + .is_some()); + // Now if we confirm one, it'll be marked as such. let height = 174500; conn.confirm_coins(&[(coin_a.outpoint, height)]); @@ -501,6 +546,10 @@ mod tests { .collect(); assert!(!outpoints.contains(&coin_a.outpoint)); assert!(outpoints.contains(&coin_b.outpoint)); + + // Both are still in DB + let coins = conn.db_coins(&[coin_a.outpoint, coin_b.outpoint]); + assert_eq!(coins.len(), 2); } fs::remove_dir_all(&tmp_dir).unwrap(); diff --git a/src/testutils.rs b/src/testutils.rs index d97b1fec..27d34775 100644 --- a/src/testutils.rs +++ b/src/testutils.rs @@ -137,6 +137,21 @@ impl DatabaseConnection for DummyDbConn { fn derivation_index_by_address(&mut self, _: &bitcoin::Address) -> Option { None } + + fn coins_by_outpoints( + &mut self, + outpoints: &[bitcoin::OutPoint], + ) -> HashMap { + // Very inefficient but hey + self.db + .read() + .unwrap() + .coins + .clone() + .into_iter() + .filter(|(op, _)| outpoints.contains(&op)) + .collect() + } } pub struct DummyMinisafe {