database: allow to query coins by their spending status

This commit is contained in:
Antoine Poinsot 2022-12-08 16:05:47 +01:00
parent b3337193a4
commit 54410cd9c4
No known key found for this signature in database
GPG Key ID: E13FC145CD3F4304
5 changed files with 61 additions and 20 deletions

View File

@ -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<secp256k1::VerifyOnly>,
) -> 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.

View File

@ -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<ListCoinsEntry> = db_conn
.coins()
.coins(CoinType::All)
// Can't use into_values as of Rust 1.48
.into_iter()
.map(|(_, coin)| {

View File

@ -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<bitcoin::OutPoint, Coin>;
fn coins(&mut self, coin_type: CoinType) -> HashMap<bitcoin::OutPoint, Coin>;
/// List coins that are being spent and whose spending transaction is still unconfirmed.
fn list_spending_coins(&mut self) -> HashMap<bitcoin::OutPoint, Coin>;
@ -178,8 +178,8 @@ impl DatabaseConnection for SqliteConn {
self.complete_wallet_rescan()
}
fn coins(&mut self) -> HashMap<bitcoin::OutPoint, Coin> {
self.coins()
fn coins(&mut self, coin_type: CoinType) -> HashMap<bitcoin::OutPoint, Coin> {
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,
}

View File

@ -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<DbCoin> {
pub fn coins(&mut self, coin_type: CoinType) -> Vec<DbCoin> {
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<bitcoin::OutPoint> =
conn.coins().into_iter().map(|c| c.outpoint).collect();
let outpoints: HashSet<bitcoin::OutPoint> = 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<bitcoin::OutPoint, DbCoin> =
conn.coins().into_iter().map(|c| (c.outpoint, c)).collect();
let coins_map: HashMap<bitcoin::OutPoint, DbCoin> = 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<bitcoin::OutPoint> = 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;

View File

@ -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<bitcoin::OutPoint, Coin> {
self.db.read().unwrap().coins.clone()
fn coins(&mut self, coin_type: CoinType) -> HashMap<bitcoin::OutPoint, Coin> {
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<bitcoin::OutPoint, Coin> {