diff --git a/src/database/mod.rs b/src/database/mod.rs index 6d275a96..375d8ad0 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -109,6 +109,9 @@ pub trait DatabaseConnection { /// 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)]); @@ -232,6 +235,10 @@ impl DatabaseConnection for SqliteConn { 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) } diff --git a/src/database/sqlite/mod.rs b/src/database/sqlite/mod.rs index 07d5f69c..b51b6a30 100644 --- a/src/database/sqlite/mod.rs +++ b/src/database/sqlite/mod.rs @@ -509,6 +509,27 @@ impl SqliteConn { .expect("Database must be available") } + /// Mark a set of coins as not being spent. + pub fn unspend_coins<'a>( + &mut self, + outpoints: impl IntoIterator, + ) { + db_exec(&mut self.conn, |db_tx| { + for outpoint in outpoints { + db_tx.execute( + "UPDATE coins SET spend_txid = NULL, spend_block_height = NULL, spend_block_time = NULL WHERE txid = ?1 AND vout = ?2", + rusqlite::params![ + outpoint.txid[..].to_vec(), + outpoint.vout, + ], + )?; + } + + Ok(()) + }) + .expect("Database must be available") + } + /// Mark the Spend transaction of a given set of coins as being confirmed at a given /// block. pub fn confirm_spend<'a>( @@ -1350,18 +1371,27 @@ CREATE TABLE spend_transactions ( coin_a.outpoint, bitcoin::Txid::from_slice(&[0; 32][..]).unwrap(), )]); - let coins_map: HashMap = conn - .coins(&[], &[]) + let coin = conn + .coins(&[], &[coin_a.outpoint]) .into_iter() - .map(|c| (c.outpoint, c)) - .collect(); - assert!(coins_map - .get(&coin_a.outpoint) - .unwrap() - .spend_txid - .is_some()); + .next() + .unwrap(); + assert!(coin.spend_txid.is_some()); - // We will see it as 'spending' + // We can unspend it, if the spend transaction gets double spent. + conn.unspend_coins(&[coin_a.outpoint]); + let coin = conn + .coins(&[], &[coin_a.outpoint]) + .into_iter() + .next() + .unwrap(); + assert!(coin.spend_txid.is_none()); + + // Spend it back. We will see it as 'spending' + conn.spend_coins(&[( + coin_a.outpoint, + bitcoin::Txid::from_slice(&[0; 32][..]).unwrap(), + )]); let outpoints: HashSet = conn .list_spending_coins() .into_iter() @@ -1406,6 +1436,16 @@ CREATE TABLE spend_transactions ( assert_eq!(coin.spend_block.as_ref().unwrap().time, time); assert_eq!(coin.spend_block.unwrap().height, height); + // If we unspend it all spend info will be wiped. + conn.unspend_coins(&[coin_a.outpoint]); + let coin = conn + .coins(&[], &[coin_a.outpoint]) + .into_iter() + .next() + .unwrap(); + assert!(coin.spend_txid.is_none()); + assert!(coin.spend_block.is_none()); + // Add an immature coin. As all coins it's first registered as unconfirmed (even though // it's not). let coin_imma = Coin { diff --git a/src/testutils.rs b/src/testutils.rs index ffc75a64..b2249cfd 100644 --- a/src/testutils.rs +++ b/src/testutils.rs @@ -278,6 +278,16 @@ impl DatabaseConnection for DummyDatabase { } } + fn unspend_coins<'a>(&mut self, outpoints: &[bitcoin::OutPoint]) { + for op in outpoints { + let mut db = self.db.write().unwrap(); + let spent = &mut db.coins.get_mut(op).unwrap(); + assert!(spent.spend_txid.is_some()); + spent.spend_txid = None; + spent.spend_block = None; + } + } + fn confirm_spend<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, bitcoin::Txid, i32, u32)]) { for (op, spend_txid, height, time) in outpoints { let mut db = self.db.write().unwrap();