Skip to content

Commit

Permalink
commands: get wallet transactions from db
Browse files Browse the repository at this point in the history
  • Loading branch information
jp1ac4 committed Jul 18, 2024
1 parent 55656ff commit a28ef39
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 43 deletions.
91 changes: 49 additions & 42 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -990,39 +990,19 @@ impl DaemonControl {
limit: u64,
) -> ListTransactionsResult {
let mut db_conn = self.db.connection();
// Note the result could in principle be retrieved in a single database query.
let txids = db_conn.list_txids(start, end, limit);
let transactions = txids
.iter()
.filter_map(|txid| {
// TODO: batch those calls to the Bitcoin backend
// so it can in turn optimize its queries.
self.bitcoin
.wallet_transaction(txid)
.map(|(tx, block)| TransactionInfo {
tx,
height: block.map(|b| b.height),
time: block.map(|b| b.time),
})
})
.collect();
ListTransactionsResult { transactions }
self.list_transactions(&txids)
}

/// list_transactions retrieves the transactions with the given txids.
pub fn list_transactions(&self, txids: &[bitcoin::Txid]) -> ListTransactionsResult {
let transactions = txids
.iter()
.filter_map(|txid| {
// TODO: batch those calls to the Bitcoin backend
// so it can in turn optimize its queries.
self.bitcoin
.wallet_transaction(txid)
.map(|(tx, block)| TransactionInfo {
tx,
height: block.map(|b| b.height),
time: block.map(|b| b.time),
})
})
let transactions = self
.db
.connection()
.list_wallet_transactions(txids)
.into_iter()
.map(|(tx, height, time)| TransactionInfo { tx, height, time })
.collect();
ListTransactionsResult { transactions }
}
Expand Down Expand Up @@ -2278,8 +2258,8 @@ mod tests {
},
]);

let mut btc = DummyBitcoind::new();
btc.txs.insert(
let mut txs_map = HashMap::new();
txs_map.insert(
deposit1.txid(),
(
deposit1.clone(),
Expand All @@ -2293,7 +2273,7 @@ mod tests {
}),
),
);
btc.txs.insert(
txs_map.insert(
deposit2.txid(),
(
deposit2.clone(),
Expand All @@ -2307,7 +2287,7 @@ mod tests {
}),
),
);
btc.txs.insert(
txs_map.insert(
spend_tx.txid(),
(
spend_tx.clone(),
Expand All @@ -2321,7 +2301,7 @@ mod tests {
}),
),
);
btc.txs.insert(
txs_map.insert(
deposit3.txid(),
(
deposit3.clone(),
Expand All @@ -2336,11 +2316,15 @@ mod tests {
),
);

let ms = DummyLiana::new(btc, db);
let ms = DummyLiana::new(DummyBitcoind::new(), db);

let control = &ms.control();
let mut db_conn = control.db.connection();
let txs: Vec<_> = txs_map.values().map(|(tx, _)| tx.clone()).collect();
db_conn.new_txs(&txs);

let transactions = control.list_confirmed_transactions(0, 4, 10).transactions;
let mut transactions = control.list_confirmed_transactions(0, 4, 10).transactions;
transactions.sort_by(|tx1, tx2| tx2.height.cmp(&tx1.height));
assert_eq!(transactions.len(), 4);

assert_eq!(transactions[0].time, Some(4));
Expand All @@ -2355,7 +2339,8 @@ mod tests {
assert_eq!(transactions[3].time, Some(1));
assert_eq!(transactions[3].tx, deposit1);

let transactions = control.list_confirmed_transactions(2, 3, 10).transactions;
let mut transactions = control.list_confirmed_transactions(2, 3, 10).transactions;
transactions.sort_by(|tx1, tx2| tx2.height.cmp(&tx1.height));
assert_eq!(transactions.len(), 2);

assert_eq!(transactions[0].time, Some(3));
Expand Down Expand Up @@ -2424,8 +2409,8 @@ mod tests {
}],
};

let mut btc = DummyBitcoind::new();
btc.txs.insert(
let mut txs_map = HashMap::new();
txs_map.insert(
tx1.txid(),
(
tx1.clone(),
Expand All @@ -2439,7 +2424,7 @@ mod tests {
}),
),
);
btc.txs.insert(
txs_map.insert(
tx2.txid(),
(
tx2.clone(),
Expand All @@ -2453,7 +2438,7 @@ mod tests {
}),
),
);
btc.txs.insert(
txs_map.insert(
tx3.txid(),
(
tx3.clone(),
Expand All @@ -2468,9 +2453,31 @@ mod tests {
),
);

let ms = DummyLiana::new(btc, DummyDatabase::new());

let ms = DummyLiana::new(DummyBitcoind::new(), DummyDatabase::new());
let control = &ms.control();
let mut db_conn = control.db.connection();
let txs: Vec<_> = txs_map.values().map(|(tx, _)| tx.clone()).collect();
db_conn.new_txs(&txs);
// We need coins in the DB in order to get the block info for the transactions.
for (txid, (_tx, block)) in txs_map {
// Insert more than one coin per transaction to check that the command does not
// return duplicate transactions.
for vout in 0..4 {
db_conn.new_unspent_coins(&[Coin {
outpoint: bitcoin::OutPoint::new(txid, vout),
is_immature: false,
block_info: block.map(|b| BlockInfo {
height: b.height,
time: b.time,
}),
amount: bitcoin::Amount::from_sat(100_000),
derivation_index: bip32::ChildNumber::from(13),
is_change: false,
spend_txid: None,
spend_block: None,
}]);
}
}

let transactions = control.list_transactions(&[tx1.txid()]).transactions;
assert_eq!(transactions.len(), 1);
Expand Down
22 changes: 22 additions & 0 deletions src/database/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ pub trait DatabaseConnection {

/// Store new transactions.
fn new_txs(&mut self, txs: &[bitcoin::Transaction]);

/// Retrieve a list of transactions and their corresponding block heights and times.
fn list_wallet_transactions(
&mut self,
txids: &[bitcoin::Txid],
) -> Vec<(bitcoin::Transaction, Option<i32>, Option<u32>)>;
}

impl DatabaseConnection for SqliteConn {
Expand Down Expand Up @@ -324,6 +330,22 @@ impl DatabaseConnection for SqliteConn {
fn new_txs<'a>(&mut self, txs: &[bitcoin::Transaction]) {
self.new_txs(txs)
}

fn list_wallet_transactions(
&mut self,
txids: &[bitcoin::Txid],
) -> Vec<(bitcoin::Transaction, Option<i32>, Option<u32>)> {
self.list_wallet_transactions(txids)
.into_iter()
.map(|wtx| {
(
wtx.transaction,
wtx.block_info.map(|b| b.height),
wtx.block_info.map(|b| b.time),
)
})
.collect()
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
Expand Down
35 changes: 34 additions & 1 deletion src/database/sqlite/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::{
sqlite::{
schema::{
DbAddress, DbCoin, DbLabel, DbLabelledKind, DbSpendTransaction, DbTip, DbWallet,
SCHEMA,
DbWalletTransaction, SCHEMA,
},
utils::{
create_fresh_db, curr_timestamp, db_exec, db_query, db_tx_query, db_version,
Expand Down Expand Up @@ -746,6 +746,39 @@ impl SqliteConn {
.expect("Database must be available")
}

pub fn list_wallet_transactions(
&mut self,
txids: &[bitcoin::Txid],
) -> Vec<DbWalletTransaction> {
// The UNION will remove duplicates.
// We assume that a transaction's block info is the same in every coins row
// it appears in.
let query = format!(
"SELECT t.tx, c.blockheight, c.blocktime \
FROM transactions t \
INNER JOIN ( \
SELECT txid, blockheight, blocktime \
FROM coins \
WHERE wallet_id = {WALLET_ID} \
UNION \
SELECT spend_txid, spend_block_height, spend_block_time \
FROM coins \
WHERE wallet_id = {WALLET_ID} \
AND spend_txid IS NOT NULL \
) c ON t.txid = c.txid \
WHERE t.txid in ({})",
txids
.iter()
.map(|txid| format!("x'{}'", FrontwardHexTxid(*txid)))
.collect::<Vec<_>>()
.join(",")
);
db_query(&mut self.conn, &query, rusqlite::params![], |row| {
row.try_into()
})
.expect("Db must not fail")
}

pub fn delete_spend(&mut self, txid: &bitcoin::Txid) {
db_exec(&mut self.conn, |db_tx| {
db_tx.execute(
Expand Down
29 changes: 29 additions & 0 deletions src/database/sqlite/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -353,3 +353,32 @@ impl TryFrom<&rusqlite::Row<'_>> for DbLabel {
})
}
}

/// A transaction together with its block info.
#[derive(Clone, Debug)]
pub struct DbWalletTransaction {
pub transaction: bitcoin::Transaction,
pub block_info: Option<DbBlockInfo>,
}

impl TryFrom<&rusqlite::Row<'_>> for DbWalletTransaction {
type Error = rusqlite::Error;

fn try_from(row: &rusqlite::Row) -> Result<Self, Self::Error> {
let transaction: Vec<u8> = row.get(0)?;
let transaction: bitcoin::Transaction =
bitcoin::consensus::deserialize(&transaction).expect("We only store valid txs");
let block_height: Option<i32> = row.get(1)?;
let block_time: Option<u32> = row.get(2)?;
assert_eq!(block_height.is_none(), block_time.is_none());
let block_info = block_height.map(|height| DbBlockInfo {
height,
time: block_time.expect("Must be there if height is"),
});

Ok(DbWalletTransaction {
transaction,
block_info,
})
}
}
32 changes: 32 additions & 0 deletions src/testutils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,38 @@ impl DatabaseConnection for DummyDatabase {
self.db.write().unwrap().txs.insert(tx.txid(), tx.clone());
}
}

fn list_wallet_transactions(
&mut self,
txids: &[bitcoin::Txid],
) -> Vec<(bitcoin::Transaction, Option<i32>, Option<u32>)> {
let txs: HashMap<_, _> = self
.db
.read()
.unwrap()
.txs
.clone()
.into_iter()
.filter(|(txid, _tx)| txids.contains(txid))
.collect();
let coins = self.coins(&[], &[]);
let mut wallet_txs = Vec::with_capacity(txs.len());
for (txid, tx) in txs {
let first_block_info = coins.values().find_map(|c| {
if c.outpoint.txid == txid {
Some(c.block_info)
} else if c.spend_txid == Some(txid) {
Some(c.spend_block)
} else {
None
}
});
if let Some(block_info) = first_block_info {
wallet_txs.push((tx, block_info.map(|b| b.height), block_info.map(|b| b.time)));
}
}
wallet_txs
}
}

pub struct DummyLiana {
Expand Down

0 comments on commit a28ef39

Please sign in to comment.