Merge #29: Check spend status in bitcoin poller
172cda19a0a72b77e3832f2e29ed7ff6f8062c44 bitcoin: avoid an unnecessary large clone() (Antoine Poinsot)
7513bcbf09f13f1625b989d52c52087048d90a73 bitcoind: use and_then instead of map().flatten() (Antoine Poinsot)
51f11a9e2f51945c7ec1f59a9acfd43d2f3977d3 looper: cleanup the check for spending coins' confirmation (Antoine Poinsot)
c9b6c6dedbc1e28b05543191238d0a81a92ba238 db: re-rename list_unspent_coins into unspent_coins (Antoine Poinsot)
3534e35b8721c5026b27d4d3c07b87dc6e4fa3dd bitcoin: remove erroneous block height check (Antoine Poinsot)
bb9897bdbb9926f6c3f8d9fb95339e45546372e2 Update coin txid if conflicting tx was confirmed (edouard)
3cf6bcbb98fc0034bc46ed58564e8aa4a1b6ffa6 add spent_coins to bitcoind poller (edouard)
94ee94edbdcdd917df131ad87893406f5f8597a2 Add blocktime and spent_at to coins table (edouard)
Pull request description:
Add two new columns:
- `blocktime`: timestamp of the block containing the transaction funding the coin.
- `spent_at`: timestamp of the block containing the transaction spending the coin.
Update the coin `spent_at` when the spend transaction is confirmed
ACKs for top commit:
darosior:
ACK 172cda19a0a72b77e3832f2e29ed7ff6f8062c44
Tree-SHA512: 4f12dd273784c1e8fab7f6427800fd10e6404d47e07e2293106d4454165dffb856cd65c5f4e4537867be403bd1790ce363968bfb94fec58ab02af3624ed68f22
This commit is contained in:
commit
2a3214460e
@ -748,9 +748,11 @@ impl From<Json> for LSBlockRes {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GetTxRes {
|
||||
pub conflicting_txs: Vec<bitcoin::Txid>,
|
||||
pub block_height: Option<i32>,
|
||||
pub block_time: Option<u32>,
|
||||
}
|
||||
|
||||
impl From<Json> for GetTxRes {
|
||||
@ -759,6 +761,26 @@ impl From<Json> for GetTxRes {
|
||||
.get("blockheight")
|
||||
.and_then(Json::as_i64)
|
||||
.map(|bh| bh as i32);
|
||||
GetTxRes { block_height }
|
||||
let block_time = json
|
||||
.get("blocktime")
|
||||
.and_then(Json::as_u64)
|
||||
.map(|bt| bt as u32);
|
||||
let conflicting_txs = json
|
||||
.get("walletconflicts")
|
||||
.and_then(Json::as_array)
|
||||
.map(|array| {
|
||||
array
|
||||
.into_iter()
|
||||
.map(|v| {
|
||||
bitcoin::Txid::from_str(v.as_str().expect("wrong json format")).unwrap()
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
|
||||
GetTxRes {
|
||||
conflicting_txs: conflicting_txs.unwrap_or_default(),
|
||||
block_height,
|
||||
block_time,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ pub mod poller;
|
||||
|
||||
use d::LSBlockEntry;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync;
|
||||
|
||||
use miniscript::bitcoin::{self, hashes::Hash};
|
||||
@ -34,14 +35,23 @@ pub trait BitcoinInterface: Send {
|
||||
/// Get coins received since the specified tip.
|
||||
fn received_coins(&self, tip: &BlockChainTip) -> Vec<UTxO>;
|
||||
|
||||
/// Get all coins that were confirmed, and at what height.
|
||||
fn confirmed_coins(&self, outpoints: &[bitcoin::OutPoint]) -> Vec<(bitcoin::OutPoint, i32)>;
|
||||
/// Get all coins that were confirmed, and at what height and time.
|
||||
fn confirmed_coins(
|
||||
&self,
|
||||
outpoints: &[bitcoin::OutPoint],
|
||||
) -> Vec<(bitcoin::OutPoint, i32, u32)>;
|
||||
|
||||
/// Get all coins that were spent, and the spending txid.
|
||||
fn spent_coins(
|
||||
/// Get all coins that are being spent, and the spending txid.
|
||||
fn spending_coins(
|
||||
&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 {
|
||||
@ -91,14 +101,19 @@ impl BitcoinInterface for d::BitcoinD {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn confirmed_coins(&self, outpoints: &[bitcoin::OutPoint]) -> Vec<(bitcoin::OutPoint, i32)> {
|
||||
fn confirmed_coins(
|
||||
&self,
|
||||
outpoints: &[bitcoin::OutPoint],
|
||||
) -> Vec<(bitcoin::OutPoint, i32, u32)> {
|
||||
let mut confirmed = Vec::with_capacity(outpoints.len());
|
||||
|
||||
for op in outpoints {
|
||||
// TODO: batch those calls to gettransaction
|
||||
if let Some(res) = self.get_transaction(&op.txid) {
|
||||
if let Some(h) = res.block_height {
|
||||
confirmed.push((*op, h));
|
||||
if let Some(t) = res.block_time {
|
||||
confirmed.push((*op, h, t));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::error!("Transaction not in wallet for coin '{}'.", op);
|
||||
@ -108,7 +123,7 @@ impl BitcoinInterface for d::BitcoinD {
|
||||
confirmed
|
||||
}
|
||||
|
||||
fn spent_coins(
|
||||
fn spending_coins(
|
||||
&self,
|
||||
outpoints: &[bitcoin::OutPoint],
|
||||
) -> Vec<(bitcoin::OutPoint, bitcoin::Txid)> {
|
||||
@ -126,12 +141,72 @@ impl BitcoinInterface for d::BitcoinD {
|
||||
);
|
||||
bitcoin::Txid::from_slice(&[0; 32][..]).unwrap()
|
||||
};
|
||||
|
||||
spent.push((*op, spending_txid));
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
};
|
||||
|
||||
// There is an immutable borrow on the cache, these txs will be added once it is
|
||||
// dropped.
|
||||
let mut txs_to_cache: Vec<(bitcoin::Txid, Option<d::GetTxRes>)> = Vec::new();
|
||||
|
||||
if let Some(tx) = tx {
|
||||
if let Some(block_time) = tx.block_time {
|
||||
// TODO: make both block time and height under the same Option.
|
||||
assert!(tx.block_height.is_some());
|
||||
spent.push((*op, *txid, block_time))
|
||||
} else if !tx.conflicting_txs.is_empty() {
|
||||
for txid in &tx.conflicting_txs {
|
||||
let tx: Option<&d::GetTxRes> = match cache.get(txid) {
|
||||
Some(tx) => tx.as_ref(),
|
||||
None => {
|
||||
let tx = self.get_transaction(&txid);
|
||||
txs_to_cache.push((*txid, tx));
|
||||
txs_to_cache.last().unwrap().1.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"),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (txid, res) in txs_to_cache {
|
||||
cache.insert(txid, res);
|
||||
}
|
||||
}
|
||||
|
||||
spent
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: do we need to repeat the entire trait implemenation? Isn't there a nicer way?
|
||||
@ -156,14 +231,24 @@ impl BitcoinInterface for sync::Arc<sync::Mutex<dyn BitcoinInterface + 'static>>
|
||||
self.lock().unwrap().received_coins(tip)
|
||||
}
|
||||
|
||||
fn confirmed_coins(&self, outpoints: &[bitcoin::OutPoint]) -> Vec<(bitcoin::OutPoint, i32)> {
|
||||
fn confirmed_coins(
|
||||
&self,
|
||||
outpoints: &[bitcoin::OutPoint],
|
||||
) -> Vec<(bitcoin::OutPoint, i32, u32)> {
|
||||
self.lock().unwrap().confirmed_coins(outpoints)
|
||||
}
|
||||
|
||||
fn spending_coins(
|
||||
&self,
|
||||
outpoints: &[bitcoin::OutPoint],
|
||||
) -> Vec<(bitcoin::OutPoint, bitcoin::Txid)> {
|
||||
self.lock().unwrap().spending_coins(outpoints)
|
||||
}
|
||||
|
||||
fn spent_coins(
|
||||
&self,
|
||||
outpoints: &[bitcoin::OutPoint],
|
||||
) -> Vec<(bitcoin::OutPoint, bitcoin::Txid)> {
|
||||
outpoints: &[(bitcoin::OutPoint, bitcoin::Txid)],
|
||||
) -> Vec<(bitcoin::OutPoint, bitcoin::Txid, u32)> {
|
||||
self.lock().unwrap().spent_coins(outpoints)
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,8 +13,9 @@ use miniscript::bitcoin;
|
||||
#[derive(Debug, Clone)]
|
||||
struct UpdatedCoins {
|
||||
pub received: Vec<Coin>,
|
||||
pub confirmed: Vec<(bitcoin::OutPoint, i32)>,
|
||||
pub spent: Vec<(bitcoin::OutPoint, bitcoin::Txid)>,
|
||||
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
|
||||
@ -40,7 +41,9 @@ fn update_coins(
|
||||
amount,
|
||||
derivation_index,
|
||||
block_height: None,
|
||||
block_time: None,
|
||||
spend_txid: None,
|
||||
spent_at: None,
|
||||
};
|
||||
received.push(coin);
|
||||
}
|
||||
@ -83,11 +86,24 @@ fn update_coins(
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let spent = bit.spent_coins(&to_be_spent);
|
||||
let spending = bit.spending_coins(&to_be_spent);
|
||||
|
||||
// Mark coins in a spending state whose Spend transaction was confirmed as such. Note we
|
||||
// need to take into account the freshly marked as spending coins as well, as their spend
|
||||
// may have been confirmed within the previous tip and the current one, and we may not poll
|
||||
// this chunk of the chain anymore.
|
||||
let spending_coins: Vec<(bitcoin::OutPoint, bitcoin::Txid)> = db_conn
|
||||
.list_spending_coins()
|
||||
.values()
|
||||
.map(|coin| (coin.outpoint, coin.spend_txid.expect("Coin is spending")))
|
||||
.chain(spending.iter().cloned())
|
||||
.collect();
|
||||
let spent = bit.spent_coins(spending_coins.as_slice());
|
||||
|
||||
UpdatedCoins {
|
||||
received,
|
||||
confirmed,
|
||||
spending,
|
||||
spent,
|
||||
}
|
||||
}
|
||||
@ -136,7 +152,8 @@ fn updates(bit: &impl BitcoinInterface, db: &impl DatabaseInterface) {
|
||||
// updates up to this block. But not more.
|
||||
db_conn.new_unspent_coins(&updated_coins.received);
|
||||
db_conn.confirm_coins(&updated_coins.confirmed);
|
||||
db_conn.spend_coins(&updated_coins.spent);
|
||||
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);
|
||||
}
|
||||
|
||||
@ -568,9 +568,11 @@ mod tests {
|
||||
db_conn.new_unspent_coins(&[Coin {
|
||||
outpoint: dummy_op,
|
||||
block_height: None,
|
||||
block_time: None,
|
||||
amount: bitcoin::Amount::from_sat(100_000),
|
||||
derivation_index: bip32::ChildNumber::from(13),
|
||||
spend_txid: None,
|
||||
spent_at: None,
|
||||
}]);
|
||||
let res = control.create_spend(&[dummy_op], &destinations, 1).unwrap();
|
||||
let tx = res.psbt.global.unsigned_tx;
|
||||
@ -660,16 +662,20 @@ mod tests {
|
||||
Coin {
|
||||
outpoint: dummy_op_a,
|
||||
block_height: None,
|
||||
block_time: None,
|
||||
amount: bitcoin::Amount::from_sat(100_000),
|
||||
derivation_index: bip32::ChildNumber::from(13),
|
||||
spend_txid: None,
|
||||
spent_at: None,
|
||||
},
|
||||
Coin {
|
||||
outpoint: dummy_op_b,
|
||||
block_height: None,
|
||||
block_time: None,
|
||||
amount: bitcoin::Amount::from_sat(115_680),
|
||||
derivation_index: bip32::ChildNumber::from(34),
|
||||
spend_txid: None,
|
||||
spent_at: None,
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@ -57,15 +57,21 @@ pub trait DatabaseConnection {
|
||||
/// Get all UTxOs.
|
||||
fn 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]);
|
||||
|
||||
/// Mark a set of coins as being confirmed at a specified height.
|
||||
fn confirm_coins<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, i32)]);
|
||||
/// 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.
|
||||
fn confirm_spend<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, bitcoin::Txid, u32)]);
|
||||
|
||||
/// Get specific coins from the database.
|
||||
fn coins_by_outpoints(
|
||||
&mut self,
|
||||
@ -84,33 +90,6 @@ pub trait DatabaseConnection {
|
||||
fn delete_spend(&mut self, txid: &bitcoin::Txid);
|
||||
}
|
||||
|
||||
// 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,
|
||||
amount,
|
||||
derivation_index,
|
||||
spend_txid,
|
||||
..
|
||||
} = db_coin;
|
||||
(
|
||||
outpoint,
|
||||
Coin {
|
||||
outpoint,
|
||||
block_height,
|
||||
amount,
|
||||
derivation_index,
|
||||
spend_txid,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl DatabaseConnection for SqliteConn {
|
||||
fn chain_tip(&mut self) -> Option<BlockChainTip> {
|
||||
match self.db_tip() {
|
||||
@ -140,14 +119,24 @@ impl DatabaseConnection for SqliteConn {
|
||||
}
|
||||
|
||||
fn unspent_coins(&mut self) -> HashMap<bitcoin::OutPoint, Coin> {
|
||||
db_coins_into_coins(self.unspent_coins())
|
||||
self.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]) {
|
||||
self.new_unspent_coins(coins)
|
||||
}
|
||||
|
||||
fn confirm_coins<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, i32)]) {
|
||||
fn confirm_coins<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, i32, u32)]) {
|
||||
self.confirm_coins(outpoints)
|
||||
}
|
||||
|
||||
@ -155,6 +144,10 @@ impl DatabaseConnection for SqliteConn {
|
||||
self.spend_coins(outpoints)
|
||||
}
|
||||
|
||||
fn confirm_spend<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, bitcoin::Txid, u32)]) {
|
||||
self.confirm_spend(outpoints)
|
||||
}
|
||||
|
||||
fn derivation_index_by_address(
|
||||
&mut self,
|
||||
address: &bitcoin::Address,
|
||||
@ -167,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> {
|
||||
@ -194,9 +190,35 @@ impl DatabaseConnection for SqliteConn {
|
||||
pub struct Coin {
|
||||
pub outpoint: bitcoin::OutPoint,
|
||||
pub block_height: Option<i32>,
|
||||
pub block_time: Option<u32>,
|
||||
pub amount: bitcoin::Amount,
|
||||
pub derivation_index: bip32::ChildNumber,
|
||||
pub spend_txid: Option<bitcoin::Txid>,
|
||||
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 {
|
||||
|
||||
@ -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.
|
||||
@ -290,13 +301,13 @@ impl SqliteConn {
|
||||
/// Mark a set of coins as confirmed.
|
||||
pub fn confirm_coins<'a>(
|
||||
&mut self,
|
||||
outpoints: impl IntoIterator<Item = &'a (bitcoin::OutPoint, i32)>,
|
||||
outpoints: impl IntoIterator<Item = &'a (bitcoin::OutPoint, i32, u32)>,
|
||||
) {
|
||||
db_exec(&mut self.conn, |db_tx| {
|
||||
for (outpoint, height) in outpoints {
|
||||
for (outpoint, height, time) in outpoints {
|
||||
db_tx.execute(
|
||||
"UPDATE coins SET blockheight = ?1 WHERE txid = ?2 AND vout = ?3",
|
||||
rusqlite::params![height, outpoint.txid.to_vec(), outpoint.vout,],
|
||||
"UPDATE coins SET blockheight = ?1, blocktime = ?2 WHERE txid = ?3 AND vout = ?4",
|
||||
rusqlite::params![height, time, outpoint.txid.to_vec(), outpoint.vout,],
|
||||
)?;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
@ -528,9 +562,11 @@ mod tests {
|
||||
)
|
||||
.unwrap(),
|
||||
block_height: None,
|
||||
block_time: None,
|
||||
amount: bitcoin::Amount::from_sat(98765),
|
||||
derivation_index: bip32::ChildNumber::from_normal_idx(10).unwrap(),
|
||||
spend_txid: None,
|
||||
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);
|
||||
@ -547,9 +583,11 @@ mod tests {
|
||||
)
|
||||
.unwrap(),
|
||||
block_height: None,
|
||||
block_time: None,
|
||||
amount: bitcoin::Amount::from_sat(1111),
|
||||
derivation_index: bip32::ChildNumber::from_normal_idx(103).unwrap(),
|
||||
spend_txid: None,
|
||||
spent_at: None,
|
||||
};
|
||||
conn.new_unspent_coins(&[coin_b.clone()]);
|
||||
let outpoints: HashSet<bitcoin::OutPoint> = conn
|
||||
@ -580,10 +618,13 @@ mod tests {
|
||||
|
||||
// Now if we confirm one, it'll be marked as such.
|
||||
let height = 174500;
|
||||
conn.confirm_coins(&[(coin_a.outpoint, height)]);
|
||||
let time = 174500;
|
||||
conn.confirm_coins(&[(coin_a.outpoint, height, time)]);
|
||||
let coins = conn.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());
|
||||
assert!(coins[1].block_time.is_none());
|
||||
|
||||
// Now if we spend one, we'll only get the other one.
|
||||
conn.spend_coins(&[(
|
||||
@ -598,6 +639,27 @@ mod tests {
|
||||
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);
|
||||
|
||||
@ -35,11 +35,14 @@ CREATE TABLE coins (
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
wallet_id INTEGER NOT NULL,
|
||||
blockheight INTEGER,
|
||||
blocktime INTEGER,
|
||||
txid BLOB NOT NULL,
|
||||
vout INTEGER NOT NULL,
|
||||
amount_sat INTEGER NOT NULL,
|
||||
derivation_index INTEGER NOT NULL,
|
||||
spend_txid BLOB,
|
||||
/* Time of the block containing the transaction spending the coin, NULL if not confirmed */
|
||||
spent_at INTEGER,
|
||||
UNIQUE (txid, vout),
|
||||
FOREIGN KEY (wallet_id) REFERENCES wallets (id)
|
||||
ON UPDATE RESTRICT
|
||||
@ -129,9 +132,11 @@ pub struct DbCoin {
|
||||
pub wallet_id: i64,
|
||||
pub outpoint: bitcoin::OutPoint,
|
||||
pub block_height: Option<i32>,
|
||||
pub block_time: Option<u32>,
|
||||
pub amount: bitcoin::Amount,
|
||||
pub derivation_index: bip32::ChildNumber,
|
||||
pub spend_txid: Option<bitcoin::Txid>,
|
||||
pub spent_at: Option<u32>,
|
||||
}
|
||||
|
||||
impl std::hash::Hash for DbCoin {
|
||||
@ -148,28 +153,32 @@ impl TryFrom<&rusqlite::Row<'_>> for DbCoin {
|
||||
let wallet_id = row.get(1)?;
|
||||
|
||||
let block_height = row.get(2)?;
|
||||
let txid: Vec<u8> = row.get(3)?;
|
||||
let block_time = row.get(3)?;
|
||||
let txid: Vec<u8> = row.get(4)?;
|
||||
let txid: bitcoin::Txid = encode::deserialize(&txid).expect("We only store valid txids");
|
||||
let vout = row.get(4)?;
|
||||
let vout = row.get(5)?;
|
||||
let outpoint = bitcoin::OutPoint { txid, vout };
|
||||
|
||||
let amount = row.get(5)?;
|
||||
let amount = row.get(6)?;
|
||||
let amount = bitcoin::Amount::from_sat(amount);
|
||||
let der_idx: u32 = row.get(6)?;
|
||||
let der_idx: u32 = row.get(7)?;
|
||||
let derivation_index = bip32::ChildNumber::from(der_idx);
|
||||
|
||||
let spend_txid: Option<Vec<u8>> = row.get(7)?;
|
||||
let spend_txid: Option<Vec<u8>> = row.get(8)?;
|
||||
let spend_txid =
|
||||
spend_txid.map(|txid| encode::deserialize(&txid).expect("We only store valid txids"));
|
||||
let spent_at = row.get(9)?;
|
||||
|
||||
Ok(DbCoin {
|
||||
id,
|
||||
wallet_id,
|
||||
outpoint,
|
||||
block_height,
|
||||
block_time,
|
||||
amount,
|
||||
derivation_index,
|
||||
spend_txid,
|
||||
spent_at,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,11 +48,18 @@ impl BitcoinInterface for DummyBitcoind {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn confirmed_coins(&self, _: &[bitcoin::OutPoint]) -> Vec<(bitcoin::OutPoint, i32)> {
|
||||
fn confirmed_coins(&self, _: &[bitcoin::OutPoint]) -> Vec<(bitcoin::OutPoint, i32, u32)> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn spent_coins(&self, _: &[bitcoin::OutPoint]) -> Vec<(bitcoin::OutPoint, bitcoin::Txid)> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@ -111,6 +118,16 @@ impl DatabaseConnection for DummyDbConn {
|
||||
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
|
||||
@ -121,21 +138,35 @@ impl DatabaseConnection for DummyDbConn {
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm_coins<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, i32)]) {
|
||||
for (op, height) in outpoints {
|
||||
fn confirm_coins<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, i32, u32)]) {
|
||||
for (op, height, time) in outpoints {
|
||||
let mut db = self.db.write().unwrap();
|
||||
let h = &mut db.coins.get_mut(op).unwrap().block_height;
|
||||
assert!(h.is_none());
|
||||
*h = Some(*height);
|
||||
let coin = &mut db.coins.get_mut(op).unwrap();
|
||||
assert!(coin.block_height.is_none());
|
||||
assert!(coin.block_time.is_none());
|
||||
coin.block_height = Some(*height);
|
||||
coin.block_time = Some(*time);
|
||||
}
|
||||
}
|
||||
|
||||
fn spend_coins<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, bitcoin::Txid)]) {
|
||||
for (op, spend_txid) in outpoints {
|
||||
let mut db = self.db.write().unwrap();
|
||||
let spender = &mut db.coins.get_mut(op).unwrap().spend_txid;
|
||||
assert!(spender.is_none());
|
||||
*spender = Some(*spend_txid);
|
||||
let spent = &mut db.coins.get_mut(op).unwrap();
|
||||
assert!(spent.spend_txid.is_none());
|
||||
assert!(spent.spent_at.is_none());
|
||||
spent.spend_txid = Some(*spend_txid);
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm_spend<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, bitcoin::Txid, u32)]) {
|
||||
for (op, spend_txid, time) in outpoints {
|
||||
let mut db = self.db.write().unwrap();
|
||||
let spent = &mut db.coins.get_mut(op).unwrap();
|
||||
assert!(!spent.spend_txid.is_none());
|
||||
assert!(spent.spent_at.is_none());
|
||||
spent.spend_txid = Some(*spend_txid);
|
||||
spent.spent_at = Some(*time);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user