add spent_coins to bitcoind poller
This commit is contained in:
parent
94ee94edbd
commit
3cf6bcbb98
@ -6,6 +6,7 @@ pub mod poller;
|
||||
|
||||
use d::LSBlockEntry;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync;
|
||||
|
||||
use miniscript::bitcoin::{self, hashes::Hash};
|
||||
@ -45,6 +46,12 @@ pub trait BitcoinInterface: Send {
|
||||
&self,
|
||||
outpoints: &[bitcoin::OutPoint],
|
||||
) -> Vec<(bitcoin::OutPoint, bitcoin::Txid)>;
|
||||
|
||||
/// Get all coins that are spent with the final spend tx txid and blocktime.
|
||||
fn spent_coins(
|
||||
&self,
|
||||
outpoints: &[(bitcoin::OutPoint, bitcoin::Txid)],
|
||||
) -> Vec<(bitcoin::OutPoint, bitcoin::Txid, u32)>;
|
||||
}
|
||||
|
||||
impl BitcoinInterface for d::BitcoinD {
|
||||
@ -141,6 +148,39 @@ impl BitcoinInterface for d::BitcoinD {
|
||||
|
||||
spent
|
||||
}
|
||||
|
||||
fn spent_coins(
|
||||
&self,
|
||||
outpoints: &[(bitcoin::OutPoint, bitcoin::Txid)],
|
||||
) -> Vec<(bitcoin::OutPoint, bitcoin::Txid, u32)> {
|
||||
let mut spent = Vec::with_capacity(outpoints.len());
|
||||
|
||||
let mut cache: HashMap<bitcoin::Txid, Option<d::GetTxRes>> = HashMap::new();
|
||||
|
||||
for (op, txid) in outpoints {
|
||||
let tx: Option<&d::GetTxRes> = match cache.get(txid) {
|
||||
Some(tx) => tx.as_ref(),
|
||||
None => {
|
||||
let tx = self.get_transaction(txid);
|
||||
cache.insert(*txid, tx);
|
||||
cache.get(txid).unwrap().as_ref()
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(tx) = tx {
|
||||
if let Some(block_height) = tx.block_height {
|
||||
if block_height > 1 {
|
||||
spent.push((*op, *txid, tx.block_time.expect("Spend is confirmed")))
|
||||
}
|
||||
} else {
|
||||
// TODO: handle the case where new transaction which txid is not the
|
||||
// coin spending_txid, spent the coin.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
spent
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: do we need to repeat the entire trait implemenation? Isn't there a nicer way?
|
||||
@ -178,6 +218,13 @@ impl BitcoinInterface for sync::Arc<sync::Mutex<dyn BitcoinInterface + 'static>>
|
||||
) -> Vec<(bitcoin::OutPoint, bitcoin::Txid)> {
|
||||
self.lock().unwrap().spending_coins(outpoints)
|
||||
}
|
||||
|
||||
fn spent_coins(
|
||||
&self,
|
||||
outpoints: &[(bitcoin::OutPoint, bitcoin::Txid)],
|
||||
) -> Vec<(bitcoin::OutPoint, bitcoin::Txid, u32)> {
|
||||
self.lock().unwrap().spent_coins(outpoints)
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: We could avoid this type (and all the conversions entailing allocations) if bitcoind
|
||||
|
||||
@ -15,6 +15,7 @@ struct UpdatedCoins {
|
||||
pub received: Vec<Coin>,
|
||||
pub confirmed: Vec<(bitcoin::OutPoint, i32, u32)>,
|
||||
pub spending: Vec<(bitcoin::OutPoint, bitcoin::Txid)>,
|
||||
pub spent: Vec<(bitcoin::OutPoint, bitcoin::Txid, u32)>,
|
||||
}
|
||||
|
||||
// Update the state of our coins. There may be new unspent, and existing ones may become confirmed
|
||||
@ -27,7 +28,7 @@ fn update_coins(
|
||||
previous_tip: &BlockChainTip,
|
||||
) -> UpdatedCoins {
|
||||
// Start by fetching newly received coins.
|
||||
let curr_coins = db_conn.unspent_coins();
|
||||
let curr_coins = db_conn.list_unspent_coins();
|
||||
let mut received = Vec::new();
|
||||
for utxo in bit.received_coins(&previous_tip) {
|
||||
if let Some(derivation_index) = db_conn.derivation_index_by_address(&utxo.address) {
|
||||
@ -87,10 +88,21 @@ fn update_coins(
|
||||
.collect();
|
||||
let spending = bit.spending_coins(&to_be_spent);
|
||||
|
||||
// We need to confirm coins that are currently spending and which transactions are now in a
|
||||
// block.
|
||||
let mut spending_coins: Vec<(bitcoin::OutPoint, bitcoin::Txid)> = db_conn
|
||||
.list_spending_coins()
|
||||
.values()
|
||||
.map(|coin| (coin.outpoint, coin.spend_txid.expect("Coin is spending")))
|
||||
.collect();
|
||||
spending_coins.extend(&spending);
|
||||
let spent = bit.spent_coins(spending_coins.as_slice());
|
||||
|
||||
UpdatedCoins {
|
||||
received,
|
||||
confirmed,
|
||||
spending,
|
||||
spent,
|
||||
}
|
||||
}
|
||||
|
||||
@ -139,6 +151,7 @@ fn updates(bit: &impl BitcoinInterface, db: &impl DatabaseInterface) {
|
||||
db_conn.new_unspent_coins(&updated_coins.received);
|
||||
db_conn.confirm_coins(&updated_coins.confirmed);
|
||||
db_conn.spend_coins(&updated_coins.spending);
|
||||
db_conn.confirm_spend(&updated_coins.spent);
|
||||
if let Some(tip) = new_tip {
|
||||
db_conn.update_tip(&tip);
|
||||
}
|
||||
|
||||
@ -197,7 +197,7 @@ impl DaemonControl {
|
||||
pub fn list_coins(&self) -> ListCoinsResult {
|
||||
let mut db_conn = self.db.connection();
|
||||
let coins: Vec<ListCoinsEntry> = db_conn
|
||||
.unspent_coins()
|
||||
.list_unspent_coins()
|
||||
// Can't use into_values as of Rust 1.48
|
||||
.into_iter()
|
||||
.map(|(_, coin)| {
|
||||
|
||||
@ -55,7 +55,10 @@ pub trait DatabaseConnection {
|
||||
) -> Option<bip32::ChildNumber>;
|
||||
|
||||
/// Get all UTxOs.
|
||||
fn unspent_coins(&mut self) -> HashMap<bitcoin::OutPoint, Coin>;
|
||||
fn list_unspent_coins(&mut self) -> 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>;
|
||||
|
||||
/// Store new UTxOs. Coins must not already be in database.
|
||||
fn new_unspent_coins<'a>(&mut self, coins: &[Coin]);
|
||||
@ -63,7 +66,7 @@ pub trait DatabaseConnection {
|
||||
/// Mark a set of coins as being confirmed at a specified height and block time.
|
||||
fn confirm_coins<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, i32, u32)]);
|
||||
|
||||
/// Mark a set of coins as being spent by a specified txid.
|
||||
/// Mark a set of coins as being spent by a specified txid of a pending transaction.
|
||||
fn spend_coins<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, bitcoin::Txid)]);
|
||||
|
||||
/// Mark a set of coins as spent by a specified txid at a specified block time.
|
||||
@ -115,8 +118,18 @@ impl DatabaseConnection for SqliteConn {
|
||||
self.increment_derivation_index(secp)
|
||||
}
|
||||
|
||||
fn unspent_coins(&mut self) -> HashMap<bitcoin::OutPoint, Coin> {
|
||||
db_coins_into_coins(self.unspent_coins())
|
||||
fn list_unspent_coins(&mut self) -> HashMap<bitcoin::OutPoint, Coin> {
|
||||
self.list_unspent_coins()
|
||||
.into_iter()
|
||||
.map(|db_coin| (db_coin.outpoint, db_coin.into()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn list_spending_coins(&mut self) -> HashMap<bitcoin::OutPoint, Coin> {
|
||||
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]) {
|
||||
@ -147,7 +160,10 @@ impl DatabaseConnection for SqliteConn {
|
||||
&mut self,
|
||||
outpoints: &[bitcoin::OutPoint],
|
||||
) -> HashMap<bitcoin::OutPoint, Coin> {
|
||||
db_coins_into_coins(self.db_coins(outpoints))
|
||||
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<Psbt> {
|
||||
@ -170,37 +186,6 @@ impl DatabaseConnection for SqliteConn {
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: if possible, avoid reallocating.
|
||||
fn db_coins_into_coins(db_coins: Vec<DbCoin>) -> HashMap<bitcoin::OutPoint, Coin> {
|
||||
db_coins
|
||||
.into_iter()
|
||||
.map(|db_coin| {
|
||||
let DbCoin {
|
||||
outpoint,
|
||||
block_height,
|
||||
block_time,
|
||||
amount,
|
||||
derivation_index,
|
||||
spend_txid,
|
||||
spent_at,
|
||||
..
|
||||
} = db_coin;
|
||||
(
|
||||
outpoint,
|
||||
Coin {
|
||||
outpoint,
|
||||
block_height,
|
||||
block_time,
|
||||
amount,
|
||||
derivation_index,
|
||||
spend_txid,
|
||||
spent_at,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Coin {
|
||||
pub outpoint: bitcoin::OutPoint,
|
||||
@ -212,6 +197,30 @@ pub struct Coin {
|
||||
pub spent_at: Option<u32>,
|
||||
}
|
||||
|
||||
impl std::convert::From<DbCoin> for Coin {
|
||||
fn from(db_coin: DbCoin) -> Coin {
|
||||
let DbCoin {
|
||||
outpoint,
|
||||
block_height,
|
||||
block_time,
|
||||
amount,
|
||||
derivation_index,
|
||||
spend_txid,
|
||||
spent_at,
|
||||
..
|
||||
} = db_coin;
|
||||
Coin {
|
||||
outpoint,
|
||||
block_height,
|
||||
block_time,
|
||||
amount,
|
||||
derivation_index,
|
||||
spend_txid,
|
||||
spent_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::hash::Hash for Coin {
|
||||
fn hash<H: std::hash::Hasher>(&self, h: &mut H) {
|
||||
self.outpoint.hash(h)
|
||||
|
||||
@ -253,7 +253,7 @@ impl SqliteConn {
|
||||
}
|
||||
|
||||
/// Get all UTxOs.
|
||||
pub fn unspent_coins(&mut self) -> Vec<DbCoin> {
|
||||
pub fn list_unspent_coins(&mut self) -> Vec<DbCoin> {
|
||||
db_query(
|
||||
&mut self.conn,
|
||||
"SELECT * FROM coins WHERE spend_txid is NULL",
|
||||
@ -263,6 +263,17 @@ impl SqliteConn {
|
||||
.expect("Db must not fail")
|
||||
}
|
||||
|
||||
/// List coins that are being spent and whose spending transaction is still unconfirmed.
|
||||
pub fn list_spending_coins(&mut self) -> Vec<DbCoin> {
|
||||
db_query(
|
||||
&mut self.conn,
|
||||
"SELECT * FROM coins WHERE spend_txid IS NOT NULL AND spent_at IS NULL",
|
||||
rusqlite::params![],
|
||||
|row| row.try_into(),
|
||||
)
|
||||
.expect("Db must not fail")
|
||||
}
|
||||
|
||||
// FIXME: don't take the whole coin, we don't need it.
|
||||
/// Store new, unconfirmed and unspent, coins.
|
||||
/// Will panic if given a coin that is already in DB.
|
||||
@ -323,6 +334,29 @@ impl SqliteConn {
|
||||
.expect("Database must be available")
|
||||
}
|
||||
|
||||
/// Mark a set of coins as spent.
|
||||
pub fn confirm_spend<'a>(
|
||||
&mut self,
|
||||
outpoints: impl IntoIterator<Item = &'a (bitcoin::OutPoint, bitcoin::Txid, u32)>,
|
||||
) {
|
||||
db_exec(&mut self.conn, |db_tx| {
|
||||
for (outpoint, spend_txid, time) in outpoints {
|
||||
db_tx.execute(
|
||||
"UPDATE coins SET spend_txid = ?1, spent_at = ?2 WHERE txid = ?3 AND vout = ?4",
|
||||
rusqlite::params![
|
||||
spend_txid.to_vec(),
|
||||
time,
|
||||
outpoint.txid.to_vec(),
|
||||
outpoint.vout,
|
||||
],
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.expect("Database must be available")
|
||||
}
|
||||
|
||||
pub fn db_address(&mut self, address: &bitcoin::Address) -> Option<DbAddress> {
|
||||
db_query(
|
||||
&mut self.conn,
|
||||
@ -519,7 +553,7 @@ mod tests {
|
||||
let mut conn = db.connection().unwrap();
|
||||
|
||||
// Necessarily empty at first.
|
||||
assert!(conn.unspent_coins().is_empty());
|
||||
assert!(conn.list_unspent_coins().is_empty());
|
||||
|
||||
// Add one, we'll get it.
|
||||
let coin_a = Coin {
|
||||
@ -535,7 +569,7 @@ mod tests {
|
||||
spent_at: None,
|
||||
};
|
||||
conn.new_unspent_coins(&[coin_a.clone()]); // On 1.48, arrays aren't IntoIterator
|
||||
assert_eq!(conn.unspent_coins()[0].outpoint, coin_a.outpoint);
|
||||
assert_eq!(conn.list_unspent_coins()[0].outpoint, coin_a.outpoint);
|
||||
|
||||
// We can query it by its outpoint
|
||||
let coins = conn.db_coins(&[coin_a.outpoint]);
|
||||
@ -557,7 +591,7 @@ mod tests {
|
||||
};
|
||||
conn.new_unspent_coins(&[coin_b.clone()]);
|
||||
let outpoints: HashSet<bitcoin::OutPoint> = conn
|
||||
.unspent_coins()
|
||||
.list_unspent_coins()
|
||||
.into_iter()
|
||||
.map(|c| c.outpoint)
|
||||
.collect();
|
||||
@ -586,7 +620,7 @@ mod tests {
|
||||
let height = 174500;
|
||||
let time = 174500;
|
||||
conn.confirm_coins(&[(coin_a.outpoint, height, time)]);
|
||||
let coins = conn.unspent_coins();
|
||||
let coins = conn.list_unspent_coins();
|
||||
assert_eq!(coins[0].block_height, Some(height));
|
||||
assert_eq!(coins[0].block_time, Some(time));
|
||||
assert!(coins[1].block_height.is_none());
|
||||
@ -598,13 +632,34 @@ mod tests {
|
||||
bitcoin::Txid::from_slice(&[0; 32][..]).unwrap(),
|
||||
)]);
|
||||
let outpoints: HashSet<bitcoin::OutPoint> = conn
|
||||
.unspent_coins()
|
||||
.list_unspent_coins()
|
||||
.into_iter()
|
||||
.map(|c| c.outpoint)
|
||||
.collect();
|
||||
assert!(!outpoints.contains(&coin_a.outpoint));
|
||||
assert!(outpoints.contains(&coin_b.outpoint));
|
||||
|
||||
let outpoints: HashSet<bitcoin::OutPoint> = conn
|
||||
.list_spending_coins()
|
||||
.into_iter()
|
||||
.map(|c| c.outpoint)
|
||||
.collect();
|
||||
assert!(outpoints.contains(&coin_a.outpoint));
|
||||
|
||||
// Now if we confirm the spend.
|
||||
conn.confirm_spend(&[(
|
||||
coin_a.outpoint,
|
||||
bitcoin::Txid::from_slice(&[0; 32][..]).unwrap(),
|
||||
3,
|
||||
)]);
|
||||
// the coin is not in a spending state.
|
||||
let outpoints: HashSet<bitcoin::OutPoint> = conn
|
||||
.list_spending_coins()
|
||||
.into_iter()
|
||||
.map(|c| c.outpoint)
|
||||
.collect();
|
||||
assert!(outpoints.is_empty());
|
||||
|
||||
// Both are still in DB
|
||||
let coins = conn.db_coins(&[coin_a.outpoint, coin_b.outpoint]);
|
||||
assert_eq!(coins.len(), 2);
|
||||
|
||||
@ -55,6 +55,13 @@ impl BitcoinInterface for DummyBitcoind {
|
||||
fn spending_coins(&self, _: &[bitcoin::OutPoint]) -> Vec<(bitcoin::OutPoint, bitcoin::Txid)> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn spent_coins(
|
||||
&self,
|
||||
_: &[(bitcoin::OutPoint, bitcoin::Txid)],
|
||||
) -> Vec<(bitcoin::OutPoint, bitcoin::Txid, u32)> {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DummyDb {
|
||||
@ -107,10 +114,20 @@ impl DatabaseConnection for DummyDbConn {
|
||||
self.db.write().unwrap().curr_index = next_index;
|
||||
}
|
||||
|
||||
fn unspent_coins(&mut self) -> HashMap<bitcoin::OutPoint, Coin> {
|
||||
fn list_unspent_coins(&mut self) -> HashMap<bitcoin::OutPoint, Coin> {
|
||||
self.db.read().unwrap().coins.clone()
|
||||
}
|
||||
|
||||
fn list_spending_coins(&mut self) -> HashMap<bitcoin::OutPoint, Coin> {
|
||||
let mut result = HashMap::new();
|
||||
for (k, v) in self.db.read().unwrap().coins.iter() {
|
||||
if !v.spend_txid.is_none() {
|
||||
result.insert(k.clone(), v.clone());
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn new_unspent_coins<'a>(&mut self, coins: &[Coin]) {
|
||||
for coin in coins {
|
||||
self.db
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user