Merge #1180: Save transactions in database

bf1e90e0fa7f357dc5fb20c12e454f58433dd898 sqlite: merge two migration tests (jp1ac4)
e8836757c70b67ea08a3091b960ff6164422d2b9 sqlite: add a unit test for migration between v4 and v5 (Antoine Poinsot)
af5fddfc4953eb49cf1cb0e56bbb4c947cc95364 commands: use database for TxGetter (jp1ac4)
a86d12d629c0197b9611b36a1b133cf3f7d41dc8 commands: get wallet transactions from db (jp1ac4)
afa6a5160160e265ae5dd427e9ffe6daea14885b poller: save transactions in database (jp1ac4)
ba4c819918ef218d2732e6614a62500f53b8b000 sqlite: separate DB migration from constructor (Antoine Poinsot)
50e7ffafa4b2ad008e0949e940d00a020b7fa4fa lib: setup the connection to bitcoind before the connection to SQLite. (Antoine Poinsot)

Pull request description:

  This is the first step of https://github.com/wizardsardine/liana/issues/56#issuecomment-2183063784.

  The poller will now save transactions in our own database. These transactions are selected based on the deposit and spend transactions of coins. Only the txid and transaction itself are saved, with the corresponding block height and time taken from the coins table.

  In a couple of follow-up commits, I've replaced some RPC calls to bitcoind with DB queries.

ACKs for top commit:
  darosior:
    re-ACK bf1e90e0fa7f357dc5fb20c12e454f58433dd898

Tree-SHA512: a1d0a6381efe307655b94a3ff257c58e4d921e98a7fa79e5c9f80016c19df761b10266d4122cb290b78424c5e2acefc163683fcfc948950e3c838e39ba31ba57
This commit is contained in:
Antoine Poinsot 2024-08-02 12:01:16 +02:00
commit 90df14ce57
No known key found for this signature in database
GPG Key ID: E13FC145CD3F4304
8 changed files with 1040 additions and 317 deletions

View File

@ -156,6 +156,40 @@ fn update_coins(
}
}
// Add new deposit and spend transactions to the database.
fn add_txs_to_db(
bit: &impl BitcoinInterface,
db_conn: &mut Box<dyn DatabaseConnection>,
updated_coins: &UpdatedCoins,
) {
let curr_txids: HashSet<_> = db_conn.list_saved_txids().into_iter().collect();
let mut new_txids = HashSet::new();
// First get all newly received coins that have not expired.
new_txids.extend(updated_coins.received.iter().filter_map(|c| {
if !updated_coins.expired.contains(&c.outpoint) {
Some(c.outpoint.txid)
} else {
None
}
}));
// Add spend txid for new & existing coins.
new_txids.extend(updated_coins.spending.iter().map(|(_, txid)| txid));
// Remove those txids we already have.
let missing_txids = new_txids.difference(&curr_txids);
log::debug!("Missing txids: {:?}", missing_txids);
// Now retrieve txs.
let txs: Vec<_> = missing_txids
.map(|txid| bit.wallet_transaction(txid).map(|(tx, _)| tx))
.collect::<Option<Vec<_>>>()
.expect("we must retrieve all txs");
if !txs.is_empty() {
db_conn.new_txs(&txs);
}
}
#[derive(Debug, Clone, Copy)]
enum TipUpdate {
// The best block is still the same as in the previous poll.
@ -233,6 +267,8 @@ fn updates(
return updates(db_conn, bit, descs, secp);
}
// Transactions must be added to the DB before coins due to foreign key constraints.
add_txs_to_db(bit, db_conn, &updated_coins);
// The chain tip did not change since we started our updates. Record them and the latest tip.
// Having the tip in database means that, as far as the chain is concerned, we've got all
// updates up to this block. But not more.

View File

@ -155,27 +155,33 @@ impl fmt::Display for RbfErrorInfo {
}
}
/// A wallet transaction getter which fetches the transaction from our Bitcoin backend with a cache
/// A wallet transaction getter which fetches the transaction from our database backend with a cache
/// to avoid needless redundant calls. Note the cache holds an Option<> so we also avoid redundant
/// calls when the txid isn't known by our Bitcoin backend.
struct BitcoindTxGetter<'a> {
bitcoind: &'a sync::Arc<sync::Mutex<dyn BitcoinInterface>>,
/// calls when the txid isn't known by our database backend.
struct DbTxGetter<'a> {
db: &'a sync::Arc<sync::Mutex<dyn DatabaseInterface>>,
cache: HashMap<bitcoin::Txid, Option<bitcoin::Transaction>>,
}
impl<'a> BitcoindTxGetter<'a> {
pub fn new(bitcoind: &'a sync::Arc<sync::Mutex<dyn BitcoinInterface>>) -> Self {
impl<'a> DbTxGetter<'a> {
pub fn new(db: &'a sync::Arc<sync::Mutex<dyn DatabaseInterface>>) -> Self {
Self {
bitcoind,
db,
cache: HashMap::new(),
}
}
}
impl<'a> TxGetter for BitcoindTxGetter<'a> {
impl<'a> TxGetter for DbTxGetter<'a> {
fn get_tx(&mut self, txid: &bitcoin::Txid) -> Option<bitcoin::Transaction> {
if let hash_map::Entry::Vacant(entry) = self.cache.entry(*txid) {
entry.insert(self.bitcoind.wallet_transaction(txid).map(|wtx| wtx.0));
let tx = self
.db
.connection()
.list_wallet_transactions(&[*txid])
.pop()
.map(|(tx, _, _)| tx);
entry.insert(tx);
}
self.cache.get(txid).cloned().flatten()
}
@ -457,7 +463,7 @@ impl DaemonControl {
return Err(CommandError::InvalidFeerate(feerate_vb));
}
let mut db_conn = self.db.connection();
let mut tx_getter = BitcoindTxGetter::new(&self.bitcoin);
let mut tx_getter = DbTxGetter::new(&self.db);
// Prepare the destination addresses.
let mut destinations_checked = Vec::with_capacity(destinations.len());
@ -754,7 +760,7 @@ impl DaemonControl {
feerate_vb: Option<u64>,
) -> Result<CreateSpendResult, CommandError> {
let mut db_conn = self.db.connection();
let mut tx_getter = BitcoindTxGetter::new(&self.bitcoin);
let mut tx_getter = DbTxGetter::new(&self.db);
if is_cancel && feerate_vb.is_some() {
return Err(CommandError::RbfError(RbfErrorInfo::SuperfluousFeerate));
@ -1015,39 +1021,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 }
}
@ -1068,7 +1054,7 @@ impl DaemonControl {
if feerate_vb < 1 {
return Err(CommandError::InvalidFeerate(feerate_vb));
}
let mut tx_getter = BitcoindTxGetter::new(&self.bitcoin);
let mut tx_getter = DbTxGetter::new(&self.db);
let mut db_conn = self.db.connection();
let sweep_addr = self.spend_addr(&mut db_conn, self.validate_address(address)?);
@ -1422,25 +1408,17 @@ mod tests {
#[test]
fn create_spend() {
let dummy_op = bitcoin::OutPoint::from_str(
"3753a1d74c0af8dd0a0f3b763c14faf3bd9ed03cbdf33337a074fb0e9f6c7810:0",
)
.unwrap();
let mut dummy_bitcoind = DummyBitcoind::new();
dummy_bitcoind.txs.insert(
dummy_op.txid,
(
bitcoin::Transaction {
version: TxVersion::TWO,
lock_time: absolute::LockTime::Blocks(absolute::Height::ZERO),
input: vec![],
output: vec![],
},
None,
),
);
let ms = DummyLiana::new(dummy_bitcoind, DummyDatabase::new());
let dummy_tx = bitcoin::Transaction {
version: TxVersion::TWO,
lock_time: absolute::LockTime::Blocks(absolute::Height::ZERO),
input: vec![],
output: vec![],
};
let dummy_op = bitcoin::OutPoint::new(dummy_tx.txid(), 0);
let ms = DummyLiana::new(DummyBitcoind::new(), DummyDatabase::new());
let control = &ms.control();
let mut db_conn = control.db().lock().unwrap().connection();
db_conn.new_txs(&[dummy_tx]);
// Arguments sanity checking
let dummy_addr =
@ -1471,7 +1449,6 @@ mod tests {
control.create_spend(&destinations, &[dummy_op], 1, None),
Err(CommandError::UnknownOutpoint(dummy_op))
);
let mut db_conn = control.db().lock().unwrap().connection();
db_conn.new_unspent_coins(&[Coin {
outpoint: dummy_op,
is_immature: false,
@ -2305,8 +2282,8 @@ mod tests {
},
]);
let mut btc = DummyBitcoind::new();
btc.txs.insert(
let mut txs_map = HashMap::new();
txs_map.insert(
deposit1.txid(),
(
deposit1.clone(),
@ -2320,7 +2297,7 @@ mod tests {
}),
),
);
btc.txs.insert(
txs_map.insert(
deposit2.txid(),
(
deposit2.clone(),
@ -2334,7 +2311,7 @@ mod tests {
}),
),
);
btc.txs.insert(
txs_map.insert(
spend_tx.txid(),
(
spend_tx.clone(),
@ -2348,7 +2325,7 @@ mod tests {
}),
),
);
btc.txs.insert(
txs_map.insert(
deposit3.txid(),
(
deposit3.clone(),
@ -2363,11 +2340,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));
@ -2382,7 +2363,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));
@ -2451,8 +2433,8 @@ mod tests {
}],
};
let mut btc = DummyBitcoind::new();
btc.txs.insert(
let mut txs_map = HashMap::new();
txs_map.insert(
tx1.txid(),
(
tx1.clone(),
@ -2466,7 +2448,7 @@ mod tests {
}),
),
);
btc.txs.insert(
txs_map.insert(
tx2.txid(),
(
tx2.clone(),
@ -2480,7 +2462,7 @@ mod tests {
}),
),
);
btc.txs.insert(
txs_map.insert(
tx3.txid(),
(
tx3.clone(),
@ -2495,9 +2477,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);

View File

@ -146,6 +146,18 @@ pub trait DatabaseConnection {
/// Retrieve a limited list of txids that where deposited or spent between the start and end timestamps (inclusive bounds)
fn list_txids(&mut self, start: u32, end: u32, limit: u64) -> Vec<bitcoin::Txid>;
/// Retrieves all txids from the transactions table whether or not they are referenced by a coin.
fn list_saved_txids(&mut self) -> Vec<bitcoin::Txid>;
/// Store transactions in database, ignoring any that already exist.
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 {
@ -310,6 +322,30 @@ impl DatabaseConnection for SqliteConn {
fn list_txids(&mut self, start: u32, end: u32, limit: u64) -> Vec<bitcoin::Txid> {
self.db_list_txids(start, end, limit)
}
fn list_saved_txids(&mut self) -> Vec<bitcoin::Txid> {
self.db_list_saved_txids()
}
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)]

File diff suppressed because one or more lines are too long

View File

@ -58,6 +58,12 @@ CREATE TABLE coins (
is_immature BOOLEAN NOT NULL CHECK (is_immature IN (0,1)),
UNIQUE (txid, vout),
FOREIGN KEY (wallet_id) REFERENCES wallets (id)
ON UPDATE RESTRICT
ON DELETE RESTRICT,
FOREIGN KEY (txid) REFERENCES transactions (txid)
ON UPDATE RESTRICT
ON DELETE RESTRICT,
FOREIGN KEY (spend_txid) REFERENCES transactions (txid)
ON UPDATE RESTRICT
ON DELETE RESTRICT
);
@ -71,6 +77,13 @@ CREATE TABLE addresses (
derivation_index INTEGER NOT NULL UNIQUE
);
/* Transactions for all wallets. */
CREATE TABLE transactions (
id INTEGER PRIMARY KEY NOT NULL,
txid BLOB UNIQUE NOT NULL,
tx BLOB UNIQUE NOT NULL
);
/* Transactions we created that spend some of our coins. */
CREATE TABLE spend_transactions (
id INTEGER PRIMARY KEY NOT NULL,
@ -346,3 +359,32 @@ impl TryFrom<&rusqlite::Row<'_>> for DbLabel {
})
}
}
/// A transaction together with its block info.
#[derive(Clone, Debug, PartialEq)]
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,
})
}
}

View File

@ -2,7 +2,7 @@ use crate::database::sqlite::{FreshDbOptions, SqliteDbError, DB_VERSION};
use std::{convert::TryInto, fs, path, time};
use miniscript::bitcoin::secp256k1;
use miniscript::bitcoin::{self, secp256k1};
pub const LOOK_AHEAD_LIMIT: u32 = 200;
@ -229,9 +229,80 @@ fn migrate_v3_to_v4(conn: &mut rusqlite::Connection) -> Result<(), SqliteDbError
Ok(())
}
fn migrate_v4_to_v5(
conn: &mut rusqlite::Connection,
bitcoin_txs: &[bitcoin::Transaction],
) -> Result<(), SqliteDbError> {
db_exec(conn, |db_tx| {
db_tx.execute(
"
CREATE TABLE transactions (
id INTEGER PRIMARY KEY NOT NULL,
txid BLOB UNIQUE NOT NULL,
tx BLOB UNIQUE NOT NULL
);",
rusqlite::params![],
)?;
for bitcoin_tx in bitcoin_txs {
let txid = &bitcoin_tx.txid()[..].to_vec();
let bitcoin_tx_ser = bitcoin::consensus::serialize(bitcoin_tx);
db_tx.execute(
"INSERT INTO transactions (txid, tx) VALUES (?1, ?2);",
rusqlite::params![txid, bitcoin_tx_ser,],
)?;
}
// Create new coins table with foreign key constraints on transactions table.
db_tx.execute_batch(
"
CREATE TABLE coins_new (
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,
is_change BOOLEAN NOT NULL CHECK (is_change IN (0,1)),
spend_txid BLOB,
spend_block_height INTEGER,
spend_block_time INTEGER,
is_immature BOOLEAN NOT NULL CHECK (is_immature IN (0,1)),
UNIQUE (txid, vout),
FOREIGN KEY (wallet_id) REFERENCES wallets (id)
ON UPDATE RESTRICT
ON DELETE RESTRICT,
FOREIGN KEY (txid) REFERENCES transactions (txid)
ON UPDATE RESTRICT
ON DELETE RESTRICT,
FOREIGN KEY (spend_txid) REFERENCES transactions (txid)
ON UPDATE RESTRICT
ON DELETE RESTRICT
);
INSERT INTO coins_new SELECT * FROM coins;
DROP TABLE coins;
ALTER TABLE coins_new RENAME TO coins;
UPDATE version SET version = 5;",
)
})?;
Ok(())
}
/// Check the database version and if necessary apply the migrations to upgrade it to the current
/// one.
pub fn maybe_apply_migration(db_path: &path::Path) -> Result<(), SqliteDbError> {
/// one. The `bitcoin_txs` parameter is here for the migration from versions 4 and earlier, which
/// did not store the Bitcoin transactions in database, to versions 5 and later, which do. For a
/// migration from v4 or earlier to v5 or later it is assumed the caller passes *all* necessary
/// transactions, otherwise the migration will fail.
pub fn maybe_apply_migration(
db_path: &path::Path,
bitcoin_txs: &[bitcoin::Transaction],
) -> Result<(), SqliteDbError> {
let mut conn = rusqlite::Connection::open(db_path)?;
// Iteratively apply the database migrations necessary.
@ -262,6 +333,15 @@ pub fn maybe_apply_migration(db_path: &path::Path) -> Result<(), SqliteDbError>
migrate_v3_to_v4(&mut conn)?;
log::warn!("Migration from database version 3 to version 4 successful.");
}
4 => {
log::warn!("Upgrading database from version 4 to version 5.");
log::warn!(
"Number of bitcoin transactions to be inserted: {}.",
bitcoin_txs.len()
);
migrate_v4_to_v5(&mut conn, bitcoin_txs)?;
log::warn!("Migration from database version 4 to version 5 successful.");
}
_ => return Err(SqliteDbError::UnsupportedVersion(version)),
}
}

View File

@ -23,13 +23,13 @@ use crate::{
bitcoin::{poller, BitcoinInterface},
config::Config,
database::{
sqlite::{FreshDbOptions, SqliteDb, SqliteDbError},
sqlite::{FreshDbOptions, SqliteDb, SqliteDbError, MAX_DB_VERSION_NO_TX_DB},
DatabaseInterface,
},
};
use std::{
error, fmt, fs, io, path,
collections, error, fmt, fs, io, path,
sync::{self, mpsc},
thread,
};
@ -92,6 +92,7 @@ pub enum StartupError {
DefaultDataDirNotFound,
DatadirCreation(path::PathBuf, io::Error),
MissingBitcoindConfig,
DbMigrateBitcoinTxs(&'static str),
Database(SqliteDbError),
Bitcoind(BitcoindError),
#[cfg(unix)]
@ -116,6 +117,10 @@ impl fmt::Display for StartupError {
f,
"Our Bitcoin interface is bitcoind but we have no 'bitcoind_config' entry in the configuration."
),
Self::DbMigrateBitcoinTxs(msg) => write!(
f,
"Error when migrating Bitcoin transaction from Bitcoin backend to database: {}.", msg
),
Self::Database(e) => write!(f, "Error initializing database: '{}'.", e),
Self::Bitcoind(e) => write!(f, "Error setting up bitcoind interface: '{}'.", e),
#[cfg(unix)]
@ -184,6 +189,7 @@ fn setup_sqlite(
data_dir: &path::Path,
fresh_data_dir: bool,
secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>,
bitcoind: &Option<BitcoinD>,
) -> Result<SqliteDb, StartupError> {
let db_path: path::PathBuf = [data_dir, path::Path::new("lianad.sqlite3")]
.iter()
@ -196,7 +202,35 @@ fn setup_sqlite(
} else {
None
};
// If opening an existing wallet whose database does not yet store the wallet transactions,
// query them from the Bitcoin backend before proceeding to the migration.
let sqlite = SqliteDb::new(db_path, options, secp)?;
if !fresh_data_dir {
let mut conn = sqlite.connection()?;
let wallet_txs = if conn.db_version() <= MAX_DB_VERSION_NO_TX_DB {
let bit = bitcoind.as_ref().ok_or(StartupError::DbMigrateBitcoinTxs(
"a connection to a Bitcoin backend is required",
))?;
let coins = conn.db_coins(&[]);
let coins_txids = coins
.iter()
.map(|c| c.outpoint.txid)
.chain(coins.iter().filter_map(|c| c.spend_txid))
.collect::<collections::HashSet<_>>();
coins_txids
.into_iter()
.map(|txid| bit.get_transaction(&txid).map(|res| res.tx))
.collect::<Option<Vec<_>>>()
.ok_or(StartupError::DbMigrateBitcoinTxs(
"missing transaction in Bitcoin backend",
))?
} else {
Vec::new()
};
sqlite.maybe_apply_migrations(&wallet_txs)?;
}
sqlite.sanity_check(config.bitcoin_config.network, &config.main_descriptor)?;
log::info!("Database initialized and checked.");
@ -337,7 +371,15 @@ impl DaemonHandle {
log::info!("Created a new data directory at '{}'", data_dir.display());
}
// Then set up the database
// Set up the connection to bitcoind (if using it) first as we may need it for the database
// migration when setting up SQLite below.
let bitcoind = if bitcoin.is_none() {
Some(setup_bitcoind(&config, &data_dir, fresh_data_dir)?)
} else {
None
};
// Then set up the database backend.
let db = match db {
Some(db) => sync::Arc::from(sync::Mutex::from(db)),
None => sync::Arc::from(sync::Mutex::from(setup_sqlite(
@ -345,17 +387,16 @@ impl DaemonHandle {
&data_dir,
fresh_data_dir,
&secp,
&bitcoind,
)?)) as sync::Arc<sync::Mutex<dyn DatabaseInterface>>,
};
// Now, set up the Bitcoin interface.
let bit = match bitcoin {
Some(bit) => sync::Arc::from(sync::Mutex::from(bit)),
None => sync::Arc::from(sync::Mutex::from(setup_bitcoind(
&config,
&data_dir,
fresh_data_dir,
)?)) as sync::Arc<sync::Mutex<dyn BitcoinInterface>>,
// Finally set up the Bitcoin backend.
let bit = match (bitcoin, bitcoind) {
(Some(bit), None) => sync::Arc::from(sync::Mutex::from(bit)),
(None, Some(bit)) => sync::Arc::from(sync::Mutex::from(bit))
as sync::Arc<sync::Mutex<dyn BitcoinInterface>>,
_ => unreachable!("Either bitcoind or bitcoin interface is always set."),
};
// If we are on a UNIX system and they told us to daemonize, do it now.

View File

@ -138,6 +138,7 @@ struct DummyDbState {
change_index: bip32::ChildNumber,
curr_tip: Option<BlockChainTip>,
coins: HashMap<bitcoin::OutPoint, Coin>,
txs: HashMap<bitcoin::Txid, bitcoin::Transaction>,
spend_txs: HashMap<bitcoin::Txid, (Psbt, Option<u32>)>,
timestamp: u32,
}
@ -169,6 +170,7 @@ impl DummyDatabase {
change_index: 0.into(),
curr_tip: None,
coins: HashMap::new(),
txs: HashMap::new(),
spend_txs: HashMap::new(),
timestamp: now,
})),
@ -437,6 +439,48 @@ impl DatabaseConnection for DummyDatabase {
txids_and_time.truncate(limit as usize);
txids_and_time.into_iter().map(|(txid, _)| txid).collect()
}
fn list_saved_txids(&mut self) -> Vec<bitcoin::Txid> {
self.db.read().unwrap().txs.keys().cloned().collect()
}
fn new_txs(&mut self, txs: &[bitcoin::Transaction]) {
for tx in txs {
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 {