From 104c6e1a093238045cad0c149f61f424ac0a9dc7 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Wed, 29 Mar 2023 16:28:25 +0200 Subject: [PATCH] commands: add an 'updated_at' field to listspendtxs entries --- doc/API.md | 1 + src/commands/mod.rs | 3 ++- src/database/mod.rs | 8 ++++---- src/database/sqlite/mod.rs | 11 +++++------ src/database/sqlite/utils.rs | 18 +++++++++--------- src/testutils.rs | 14 ++++++++++---- tests/test_rpc.py | 3 +++ 7 files changed, 34 insertions(+), 24 deletions(-) diff --git a/doc/API.md b/doc/API.md index bcb93b58..bc8f037e 100644 --- a/doc/API.md +++ b/doc/API.md @@ -174,6 +174,7 @@ This command does not take any parameter for now. | Field | Type | Description | | -------------- | ----------------- | ----------------------------------------------------------------------- | | `psbt` | string | Base64-encoded PSBT of the Spend transaction. | +| `updated_at` | int or null | UNIX timestamp of the last time this PSBT was updated. | ### `delspendtx` diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 1f54f900..73b3f11d 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -563,7 +563,7 @@ impl DaemonControl { let spend_txs = db_conn .list_spend() .into_iter() - .map(|psbt| ListSpendEntry { psbt }) + .map(|(psbt, updated_at)| ListSpendEntry { psbt, updated_at }) .collect(); ListSpendResult { spend_txs } } @@ -840,6 +840,7 @@ pub struct CreateSpendResult { pub struct ListSpendEntry { #[serde(serialize_with = "ser_base64", deserialize_with = "deser_base64")] pub psbt: Psbt, + pub updated_at: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src/database/mod.rs b/src/database/mod.rs index 857041c1..699cd5d8 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -112,8 +112,8 @@ pub trait DatabaseConnection { /// Insert a new Spend transaction or replace an existing one. fn store_spend(&mut self, psbt: &Psbt); - /// List all existing Spend transactions. - fn list_spend(&mut self) -> Vec; + /// List all existing Spend transactions, along with an optional last update timestamp. + fn list_spend(&mut self) -> Vec<(Psbt, Option)>; /// Delete a Spend transaction from database. fn delete_spend(&mut self, txid: &bitcoin::Txid); @@ -241,10 +241,10 @@ impl DatabaseConnection for SqliteConn { self.store_spend(psbt) } - fn list_spend(&mut self) -> Vec { + fn list_spend(&mut self) -> Vec<(Psbt, Option)> { self.list_spend() .into_iter() - .map(|db_spend| db_spend.psbt) + .map(|db_spend| (db_spend.psbt, db_spend.updated_at)) .collect() } diff --git a/src/database/sqlite/mod.rs b/src/database/sqlite/mod.rs index ed81983d..c59a79d8 100644 --- a/src/database/sqlite/mod.rs +++ b/src/database/sqlite/mod.rs @@ -15,8 +15,8 @@ use crate::{ sqlite::{ schema::{DbAddress, DbCoin, DbSpendTransaction, DbTip, DbWallet, SCHEMA}, utils::{ - create_fresh_db, db_exec, db_query, db_tx_query, db_version, maybe_apply_migration, - LOOK_AHEAD_LIMIT, + create_fresh_db, curr_timestamp, db_exec, db_query, db_tx_query, db_version, + maybe_apply_migration, LOOK_AHEAD_LIMIT, }, }, Coin, CoinType, @@ -500,9 +500,9 @@ impl SqliteConn { db_exec(&mut self.conn, |db_tx| { db_tx.execute( - "INSERT into spend_transactions (psbt, txid) VALUES (?1, ?2) \ + "INSERT into spend_transactions (psbt, txid, updated_at) VALUES (?1, ?2, ?3) \ ON CONFLICT DO UPDATE SET psbt=excluded.psbt", - rusqlite::params![psbt, txid], + rusqlite::params![psbt, txid, curr_timestamp()], )?; Ok(()) }) @@ -1463,12 +1463,11 @@ CREATE TABLE spend_transactions ( .find(|db_spend| db_spend.psbt == first_psbt) .unwrap(); assert!(first_spend.updated_at.is_none()); - // TODO: update once we update store_spend() to take a timestamp. let second_spend = db_spends .iter() .find(|db_spend| db_spend.psbt == second_psbt) .unwrap(); - assert!(second_spend.updated_at.is_none()); + assert!(second_spend.updated_at.is_some()); } fs::remove_dir_all(tmp_dir).unwrap(); diff --git a/src/database/sqlite/utils.rs b/src/database/sqlite/utils.rs index db94a114..073fbe7f 100644 --- a/src/database/sqlite/utils.rs +++ b/src/database/sqlite/utils.rs @@ -50,11 +50,14 @@ where .collect::>>() } -// Sqlite supports up to i64, thus rusqlite prevents us from inserting u64's. -// We use this to panic rather than inserting a truncated integer into the database (as we'd have -// done by using `n as u32`). -fn timestamp_to_u32(n: u64) -> u32 { - n.try_into() +/// The current time as the number of seconds since the UNIX epoch, truncated to u32 since SQLite +/// only supports i64 integers. +pub fn curr_timestamp() -> u32 { + time::SystemTime::now() + .duration_since(time::UNIX_EPOCH) + .expect("System clock went backward the epoch?") + .as_secs() + .try_into() .expect("Is this the year 2106 yet? Misconfigured system clock.") } @@ -87,10 +90,7 @@ pub fn create_fresh_db( ) -> Result<(), SqliteDbError> { create_db_file(db_path)?; - let timestamp = time::SystemTime::now() - .duration_since(time::UNIX_EPOCH) - .map(|dur| timestamp_to_u32(dur.as_secs())) - .expect("System clock went backward the epoch?"); + let timestamp = curr_timestamp(); // Fill the initial addresses. On a fresh database, the deposit_derivation_index is // necessarily 0. diff --git a/src/testutils.rs b/src/testutils.rs index d91f222d..ee5b0e3b 100644 --- a/src/testutils.rs +++ b/src/testutils.rs @@ -120,7 +120,7 @@ struct DummyDbState { change_index: bip32::ChildNumber, curr_tip: Option, coins: HashMap, - spend_txs: HashMap, + spend_txs: HashMap)>, } pub struct DummyDatabase { @@ -293,14 +293,20 @@ impl DatabaseConnection for DummyDatabase { .write() .unwrap() .spend_txs - .insert(txid, psbt.clone()); + .insert(txid, (psbt.clone(), None)); } fn spend_tx(&mut self, txid: &bitcoin::Txid) -> Option { - self.db.read().unwrap().spend_txs.get(txid).cloned() + self.db + .read() + .unwrap() + .spend_txs + .get(txid) + .cloned() + .map(|x| x.0) } - fn list_spend(&mut self) -> Vec { + fn list_spend(&mut self) -> Vec<(Psbt, Option)> { self.db .read() .unwrap() diff --git a/tests/test_rpc.py b/tests/test_rpc.py index 55c76858..c8f5fb63 100644 --- a/tests/test_rpc.py +++ b/tests/test_rpc.py @@ -159,6 +159,7 @@ def test_list_spend(lianad, bitcoind): assert "psbt" in res_b # Store them both in DB. + time_before_update = int(time.time()) assert len(lianad.rpc.listspendtxs()["spend_txs"]) == 0 lianad.rpc.updatespend(res["psbt"]) lianad.rpc.updatespend(res_b["psbt"]) @@ -168,7 +169,9 @@ def test_list_spend(lianad, bitcoind): list_res = lianad.rpc.listspendtxs()["spend_txs"] assert len(list_res) == 2 first_psbt = next(entry for entry in list_res if entry["psbt"] == res["psbt"]) + assert time_before_update <= first_psbt["updated_at"] <= int(time.time()) second_psbt = next(entry for entry in list_res if entry["psbt"] == res_b["psbt"]) + assert time_before_update <= second_psbt["updated_at"] <= int(time.time()) # If we delete the first one, we'll get only the second one. first_psbt = PSBT.from_base64(res["psbt"])