diff --git a/CHANGELOG.md b/CHANGELOG.md index fae09c56..866dc2e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Overhauled sync logic for electrum and esplora. - Unify ureq and reqwest esplora backends to have the same configuration parameters. This means reqwest now has a timeout parameter and ureq has a concurrency parameter. - Fixed esplora fee estimation. +- Update the `Database` trait to store the last sync timestamp and block height +- Rename `ConfirmationTime` to `BlockTime` ## [v0.13.0] - [v0.12.0] diff --git a/Cargo.toml b/Cargo.toml index b289f194..e4ebf4ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,7 +94,7 @@ test-md-docs = ["electrum"] lazy_static = "1.4" env_logger = "0.7" clap = "2.33" -electrsd = { version= "0.12", features = ["trigger", "bitcoind_0_21_1"] } +electrsd = { version= "0.12", features = ["trigger", "bitcoind_22_0"] } [[example]] name = "address_validator" diff --git a/src/blockchain/compact_filters/mod.rs b/src/blockchain/compact_filters/mod.rs index b513d378..c2e2b8ea 100644 --- a/src/blockchain/compact_filters/mod.rs +++ b/src/blockchain/compact_filters/mod.rs @@ -71,7 +71,7 @@ use super::{Blockchain, Capability, ConfigurableBlockchain, Progress}; use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils}; use crate::error::Error; use crate::types::{KeychainKind, LocalUtxo, TransactionDetails}; -use crate::{ConfirmationTime, FeeRate}; +use crate::{BlockTime, FeeRate}; use peer::*; use store::*; @@ -206,7 +206,7 @@ impl CompactFiltersBlockchain { transaction: Some(tx.clone()), received: incoming, sent: outgoing, - confirmation_time: ConfirmationTime::new(height, timestamp), + confirmation_time: BlockTime::new(height, timestamp), verified: height.is_some(), fee: Some(inputs_sum.saturating_sub(outputs_sum)), }; diff --git a/src/blockchain/electrum.rs b/src/blockchain/electrum.rs index 53fb6e86..cb079a87 100644 --- a/src/blockchain/electrum.rs +++ b/src/blockchain/electrum.rs @@ -37,7 +37,7 @@ use super::script_sync::Request; use super::*; use crate::database::{BatchDatabase, Database}; use crate::error::Error; -use crate::{ConfirmationTime, FeeRate}; +use crate::{BlockTime, FeeRate}; /// Wrapper over an Electrum Client that implements the required blockchain traits /// @@ -146,7 +146,7 @@ impl Blockchain for ElectrumBlockchain { .map(|height| { let timestamp = *block_times.get(height).ok_or_else(electrum_goof)?; - Result::<_, Error>::Ok(ConfirmationTime { + Result::<_, Error>::Ok(BlockTime { height: *height, timestamp: timestamp.into(), }) diff --git a/src/blockchain/esplora/api.rs b/src/blockchain/esplora/api.rs index 74c46c88..4e0e3f88 100644 --- a/src/blockchain/esplora/api.rs +++ b/src/blockchain/esplora/api.rs @@ -1,7 +1,7 @@ //! structs from the esplora API //! //! see: -use crate::ConfirmationTime; +use crate::BlockTime; use bitcoin::{OutPoint, Script, Transaction, TxIn, TxOut, Txid}; #[derive(serde::Deserialize, Clone, Debug)] @@ -78,13 +78,13 @@ impl Tx { } } - pub fn confirmation_time(&self) -> Option { + pub fn confirmation_time(&self) -> Option { match self.status { TxStatus { confirmed: true, block_height: Some(height), block_time: Some(timestamp), - } => Some(ConfirmationTime { timestamp, height }), + } => Some(BlockTime { timestamp, height }), _ => None, } } diff --git a/src/blockchain/rpc.rs b/src/blockchain/rpc.rs index d4a5beca..4cd22943 100644 --- a/src/blockchain/rpc.rs +++ b/src/blockchain/rpc.rs @@ -37,7 +37,7 @@ use crate::blockchain::{Blockchain, Capability, ConfigurableBlockchain, Progress use crate::database::{BatchDatabase, DatabaseUtils}; use crate::descriptor::{get_checksum, IntoWalletDescriptor}; use crate::wallet::utils::SecpCtx; -use crate::{ConfirmationTime, Error, FeeRate, KeychainKind, LocalUtxo, TransactionDetails}; +use crate::{BlockTime, Error, FeeRate, KeychainKind, LocalUtxo, TransactionDetails}; use bitcoincore_rpc::json::{ GetAddressInfoResultLabel, ImportMultiOptions, ImportMultiRequest, ImportMultiRequestScriptPubkey, ImportMultiRescanSince, @@ -230,7 +230,7 @@ impl Blockchain for RpcBlockchain { list_txs_ids.insert(txid); if let Some(mut known_tx) = known_txs.get_mut(&txid) { let confirmation_time = - ConfirmationTime::new(tx_result.info.blockheight, tx_result.info.blocktime); + BlockTime::new(tx_result.info.blockheight, tx_result.info.blocktime); if confirmation_time != known_tx.confirmation_time { // reorg may change tx height debug!( @@ -266,7 +266,7 @@ impl Blockchain for RpcBlockchain { let td = TransactionDetails { transaction: Some(tx), txid: tx_result.info.txid, - confirmation_time: ConfirmationTime::new( + confirmation_time: BlockTime::new( tx_result.info.blockheight, tx_result.info.blocktime, ), diff --git a/src/blockchain/script_sync.rs b/src/blockchain/script_sync.rs index e7ae2763..4c9b0222 100644 --- a/src/blockchain/script_sync.rs +++ b/src/blockchain/script_sync.rs @@ -6,7 +6,7 @@ returns associated transactions i.e. electrum. use crate::{ database::{BatchDatabase, BatchOperations, DatabaseUtils}, wallet::time::Instant, - ConfirmationTime, Error, KeychainKind, LocalUtxo, TransactionDetails, + BlockTime, Error, KeychainKind, LocalUtxo, TransactionDetails, }; use bitcoin::{OutPoint, Script, Transaction, TxOut, Txid}; use log::*; @@ -246,7 +246,7 @@ impl<'a, D: BatchDatabase> ConftimeReq<'a, D> { pub fn satisfy( mut self, - confirmation_times: Vec>, + confirmation_times: Vec>, ) -> Result, Error> { let conftime_needed = self .request() diff --git a/src/database/any.rs b/src/database/any.rs index 5186452c..8b626e4b 100644 --- a/src/database/any.rs +++ b/src/database/any.rs @@ -144,6 +144,9 @@ impl BatchOperations for AnyDatabase { fn set_last_index(&mut self, keychain: KeychainKind, value: u32) -> Result<(), Error> { impl_inner_method!(AnyDatabase, self, set_last_index, keychain, value) } + fn set_sync_time(&mut self, sync_time: SyncTime) -> Result<(), Error> { + impl_inner_method!(AnyDatabase, self, set_sync_time, sync_time) + } fn del_script_pubkey_from_path( &mut self, @@ -180,6 +183,9 @@ impl BatchOperations for AnyDatabase { fn del_last_index(&mut self, keychain: KeychainKind) -> Result, Error> { impl_inner_method!(AnyDatabase, self, del_last_index, keychain) } + fn del_sync_time(&mut self) -> Result, Error> { + impl_inner_method!(AnyDatabase, self, del_sync_time) + } } impl Database for AnyDatabase { @@ -241,6 +247,9 @@ impl Database for AnyDatabase { fn get_last_index(&self, keychain: KeychainKind) -> Result, Error> { impl_inner_method!(AnyDatabase, self, get_last_index, keychain) } + fn get_sync_time(&self) -> Result, Error> { + impl_inner_method!(AnyDatabase, self, get_sync_time) + } fn increment_last_index(&mut self, keychain: KeychainKind) -> Result { impl_inner_method!(AnyDatabase, self, increment_last_index, keychain) @@ -272,6 +281,9 @@ impl BatchOperations for AnyBatch { fn set_last_index(&mut self, keychain: KeychainKind, value: u32) -> Result<(), Error> { impl_inner_method!(AnyBatch, self, set_last_index, keychain, value) } + fn set_sync_time(&mut self, sync_time: SyncTime) -> Result<(), Error> { + impl_inner_method!(AnyBatch, self, set_sync_time, sync_time) + } fn del_script_pubkey_from_path( &mut self, @@ -302,6 +314,9 @@ impl BatchOperations for AnyBatch { fn del_last_index(&mut self, keychain: KeychainKind) -> Result, Error> { impl_inner_method!(AnyBatch, self, del_last_index, keychain) } + fn del_sync_time(&mut self) -> Result, Error> { + impl_inner_method!(AnyBatch, self, del_sync_time) + } } impl BatchDatabase for AnyDatabase { diff --git a/src/database/keyvalue.rs b/src/database/keyvalue.rs index 2da92f22..07499e9f 100644 --- a/src/database/keyvalue.rs +++ b/src/database/keyvalue.rs @@ -18,7 +18,7 @@ use bitcoin::hash_types::Txid; use bitcoin::{OutPoint, Script, Transaction}; use crate::database::memory::MapKey; -use crate::database::{BatchDatabase, BatchOperations, Database}; +use crate::database::{BatchDatabase, BatchOperations, Database, SyncTime}; use crate::error::Error; use crate::types::*; @@ -82,6 +82,13 @@ macro_rules! impl_batch_operations { Ok(()) } + fn set_sync_time(&mut self, data: SyncTime) -> Result<(), Error> { + let key = MapKey::SyncTime.as_map_key(); + self.insert(key, serde_json::to_vec(&data)?)$($after_insert)*; + + Ok(()) + } + fn del_script_pubkey_from_path(&mut self, keychain: KeychainKind, path: u32) -> Result, Error> { let key = MapKey::Path((Some(keychain), Some(path))).as_map_key(); let res = self.remove(key); @@ -168,6 +175,14 @@ macro_rules! impl_batch_operations { } } } + + fn del_sync_time(&mut self) -> Result, Error> { + let key = MapKey::SyncTime.as_map_key(); + let res = self.remove(key); + let res = $process_delete!(res); + + Ok(res.map(|b| serde_json::from_slice(&b)).transpose()?) + } } } @@ -342,6 +357,14 @@ impl Database for Tree { .transpose() } + fn get_sync_time(&self) -> Result, Error> { + let key = MapKey::SyncTime.as_map_key(); + Ok(self + .get(key)? + .map(|b| serde_json::from_slice(&b)) + .transpose()?) + } + // inserts 0 if not present fn increment_last_index(&mut self, keychain: KeychainKind) -> Result { let key = MapKey::LastIndex(keychain).as_map_key(); @@ -470,4 +493,9 @@ mod test { fn test_last_index() { crate::database::test::test_last_index(get_tree()); } + + #[test] + fn test_sync_time() { + crate::database::test::test_sync_time(get_tree()); + } } diff --git a/src/database/memory.rs b/src/database/memory.rs index 78cc031d..e828dc9d 100644 --- a/src/database/memory.rs +++ b/src/database/memory.rs @@ -22,7 +22,7 @@ use bitcoin::consensus::encode::{deserialize, serialize}; use bitcoin::hash_types::Txid; use bitcoin::{OutPoint, Script, Transaction}; -use crate::database::{BatchDatabase, BatchOperations, ConfigurableDatabase, Database}; +use crate::database::{BatchDatabase, BatchOperations, ConfigurableDatabase, Database, SyncTime}; use crate::error::Error; use crate::types::*; @@ -33,6 +33,7 @@ use crate::types::*; // transactions t -> tx details // deriv indexes c{i,e} -> u32 // descriptor checksum d{i,e} -> vec +// last sync time l -> { height, timestamp } pub(crate) enum MapKey<'a> { Path((Option, Option)), @@ -41,6 +42,7 @@ pub(crate) enum MapKey<'a> { RawTx(Option<&'a Txid>), Transaction(Option<&'a Txid>), LastIndex(KeychainKind), + SyncTime, DescriptorChecksum(KeychainKind), } @@ -59,6 +61,7 @@ impl MapKey<'_> { MapKey::RawTx(_) => b"r".to_vec(), MapKey::Transaction(_) => b"t".to_vec(), MapKey::LastIndex(st) => [b"c", st.as_ref()].concat(), + MapKey::SyncTime => b"l".to_vec(), MapKey::DescriptorChecksum(st) => [b"d", st.as_ref()].concat(), } } @@ -180,6 +183,12 @@ impl BatchOperations for MemoryDatabase { Ok(()) } + fn set_sync_time(&mut self, data: SyncTime) -> Result<(), Error> { + let key = MapKey::SyncTime.as_map_key(); + self.map.insert(key, Box::new(data)); + + Ok(()) + } fn del_script_pubkey_from_path( &mut self, @@ -270,6 +279,13 @@ impl BatchOperations for MemoryDatabase { Some(b) => Ok(Some(*b.downcast_ref().unwrap())), } } + fn del_sync_time(&mut self) -> Result, Error> { + let key = MapKey::SyncTime.as_map_key(); + let res = self.map.remove(&key); + self.deleted_keys.push(key); + + Ok(res.map(|b| b.downcast_ref().cloned().unwrap())) + } } impl Database for MemoryDatabase { @@ -407,6 +423,14 @@ impl Database for MemoryDatabase { Ok(self.map.get(&key).map(|b| *b.downcast_ref().unwrap())) } + fn get_sync_time(&self) -> Result, Error> { + let key = MapKey::SyncTime.as_map_key(); + Ok(self + .map + .get(&key) + .map(|b| b.downcast_ref().cloned().unwrap())) + } + // inserts 0 if not present fn increment_last_index(&mut self, keychain: KeychainKind) -> Result { let key = MapKey::LastIndex(keychain).as_map_key(); @@ -479,12 +503,10 @@ macro_rules! populate_test_db { }; let txid = tx.txid(); - let confirmation_time = tx_meta - .min_confirmations - .map(|conf| $crate::ConfirmationTime { - height: current_height.unwrap().checked_sub(conf as u32).unwrap(), - timestamp: 0, - }); + let confirmation_time = tx_meta.min_confirmations.map(|conf| $crate::BlockTime { + height: current_height.unwrap().checked_sub(conf as u32).unwrap(), + timestamp: 0, + }); let tx_details = $crate::TransactionDetails { transaction: Some(tx.clone()), @@ -590,4 +612,9 @@ mod test { fn test_last_index() { crate::database::test::test_last_index(get_tree()); } + + #[test] + fn test_sync_time() { + crate::database::test::test_sync_time(get_tree()); + } } diff --git a/src/database/mod.rs b/src/database/mod.rs index 4a3936f5..e160b742 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -24,6 +24,8 @@ //! //! [`Wallet`]: crate::wallet::Wallet +use serde::{Deserialize, Serialize}; + use bitcoin::hash_types::Txid; use bitcoin::{OutPoint, Script, Transaction, TxOut}; @@ -44,6 +46,15 @@ pub use sqlite::SqliteDatabase; pub mod memory; pub use memory::MemoryDatabase; +/// Blockchain state at the time of syncing +/// +/// Contains only the block time and height at the moment +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SyncTime { + /// Block timestamp and height at the time of sync + pub block_time: BlockTime, +} + /// Trait for operations that can be batched /// /// This trait defines the list of operations that must be implemented on the [`Database`] type and @@ -64,6 +75,8 @@ pub trait BatchOperations { fn set_tx(&mut self, transaction: &TransactionDetails) -> Result<(), Error>; /// Store the last derivation index for a given keychain. fn set_last_index(&mut self, keychain: KeychainKind, value: u32) -> Result<(), Error>; + /// Store the sync time + fn set_sync_time(&mut self, sync_time: SyncTime) -> Result<(), Error>; /// Delete a script_pubkey given the keychain and its child number. fn del_script_pubkey_from_path( @@ -89,6 +102,10 @@ pub trait BatchOperations { ) -> Result, Error>; /// Delete the last derivation index for a keychain. fn del_last_index(&mut self, keychain: KeychainKind) -> Result, Error>; + /// Reset the sync time to `None` + /// + /// Returns the removed value + fn del_sync_time(&mut self) -> Result, Error>; } /// Trait for reading data from a database @@ -134,6 +151,8 @@ pub trait Database: BatchOperations { fn get_tx(&self, txid: &Txid, include_raw: bool) -> Result, Error>; /// Return the last defivation index for a keychain. fn get_last_index(&self, keychain: KeychainKind) -> Result, Error>; + /// Return the sync time, if present + fn get_sync_time(&self) -> Result, Error>; /// Increment the last derivation index for a keychain and return it /// @@ -325,7 +344,7 @@ pub mod test { received: 1337, sent: 420420, fee: Some(140), - confirmation_time: Some(ConfirmationTime { + confirmation_time: Some(BlockTime { timestamp: 123456, height: 1000, }), @@ -377,5 +396,25 @@ pub mod test { ); } + pub fn test_sync_time(mut tree: D) { + assert!(tree.get_sync_time().unwrap().is_none()); + + tree.set_sync_time(SyncTime { + block_time: BlockTime { + height: 100, + timestamp: 1000, + }, + }) + .unwrap(); + + let extracted = tree.get_sync_time().unwrap(); + assert!(extracted.is_some()); + assert_eq!(extracted.as_ref().unwrap().block_time.height, 100); + assert_eq!(extracted.as_ref().unwrap().block_time.timestamp, 1000); + + tree.del_sync_time().unwrap(); + assert!(tree.get_sync_time().unwrap().is_none()); + } + // TODO: more tests... } diff --git a/src/database/sqlite.rs b/src/database/sqlite.rs index 0dbaed44..597ef654 100644 --- a/src/database/sqlite.rs +++ b/src/database/sqlite.rs @@ -13,7 +13,7 @@ use bitcoin::consensus::encode::{deserialize, serialize}; use bitcoin::hash_types::Txid; use bitcoin::{OutPoint, Script, Transaction, TxOut}; -use crate::database::{BatchDatabase, BatchOperations, Database}; +use crate::database::{BatchDatabase, BatchOperations, Database, SyncTime}; use crate::error::Error; use crate::types::*; @@ -35,6 +35,7 @@ static MIGRATIONS: &[&str] = &[ "CREATE UNIQUE INDEX idx_indices_keychain ON last_derivation_indices(keychain);", "CREATE TABLE checksums (keychain TEXT, checksum BLOB);", "CREATE INDEX idx_checksums_keychain ON checksums(keychain);", + "CREATE TABLE sync_time (id INTEGER PRIMARY KEY, height INTEGER, timestamp INTEGER);" ]; /// Sqlite database stored on filesystem @@ -205,6 +206,19 @@ impl SqliteDatabase { Ok(()) } + fn update_sync_time(&self, data: SyncTime) -> Result { + let mut statement = self.connection.prepare_cached( + "INSERT INTO sync_time (id, height, timestamp) VALUES (0, :height, :timestamp) ON CONFLICT(id) DO UPDATE SET height=:height, timestamp=:timestamp WHERE id = 0", + )?; + + statement.execute(named_params! { + ":height": data.block_time.height, + ":timestamp": data.block_time.timestamp, + })?; + + Ok(self.connection.last_insert_rowid()) + } + fn select_script_pubkeys(&self) -> Result, Error> { let mut statement = self .connection @@ -375,7 +389,7 @@ impl SqliteDatabase { }; let confirmation_time = match (height, timestamp) { - (Some(height), Some(timestamp)) => Some(ConfirmationTime { height, timestamp }), + (Some(height), Some(timestamp)) => Some(BlockTime { height, timestamp }), _ => None, }; @@ -409,7 +423,7 @@ impl SqliteDatabase { let verified: bool = row.get(6)?; let confirmation_time = match (height, timestamp) { - (Some(height), Some(timestamp)) => Some(ConfirmationTime { height, timestamp }), + (Some(height), Some(timestamp)) => Some(BlockTime { height, timestamp }), _ => None, }; @@ -452,7 +466,7 @@ impl SqliteDatabase { }; let confirmation_time = match (height, timestamp) { - (Some(height), Some(timestamp)) => Some(ConfirmationTime { height, timestamp }), + (Some(height), Some(timestamp)) => Some(BlockTime { height, timestamp }), _ => None, }; @@ -487,6 +501,24 @@ impl SqliteDatabase { } } + fn select_sync_time(&self) -> Result, Error> { + let mut statement = self + .connection + .prepare_cached("SELECT height, timestamp FROM sync_time WHERE id = 0")?; + let mut rows = statement.query([])?; + + if let Some(row) = rows.next()? { + Ok(Some(SyncTime { + block_time: BlockTime { + height: row.get(0)?, + timestamp: row.get(1)?, + }, + })) + } else { + Ok(None) + } + } + fn select_checksum_by_keychain(&self, keychain: String) -> Result>, Error> { let mut statement = self .connection @@ -563,6 +595,14 @@ impl SqliteDatabase { Ok(()) } + + fn delete_sync_time(&self) -> Result<(), Error> { + let mut statement = self + .connection + .prepare_cached("DELETE FROM sync_time WHERE id = 0")?; + statement.execute([])?; + Ok(()) + } } impl BatchOperations for SqliteDatabase { @@ -622,6 +662,11 @@ impl BatchOperations for SqliteDatabase { Ok(()) } + fn set_sync_time(&mut self, ct: SyncTime) -> Result<(), Error> { + self.update_sync_time(ct)?; + Ok(()) + } + fn del_script_pubkey_from_path( &mut self, keychain: KeychainKind, @@ -707,6 +752,17 @@ impl BatchOperations for SqliteDatabase { None => Ok(None), } } + + fn del_sync_time(&mut self) -> Result, Error> { + match self.select_sync_time()? { + Some(value) => { + self.delete_sync_time()?; + + Ok(Some(value)) + } + None => Ok(None), + } + } } impl Database for SqliteDatabase { @@ -818,6 +874,10 @@ impl Database for SqliteDatabase { Ok(value) } + fn get_sync_time(&self) -> Result, Error> { + self.select_sync_time() + } + fn increment_last_index(&mut self, keychain: KeychainKind) -> Result { let keychain_string = serde_json::to_string(&keychain)?; match self.get_last_index(keychain)? { @@ -965,4 +1025,9 @@ pub mod test { fn test_last_index() { crate::database::test::test_last_index(get_database()); } + + #[test] + fn test_sync_time() { + crate::database::test::test_sync_time(get_database()); + } } diff --git a/src/testutils/blockchain_tests.rs b/src/testutils/blockchain_tests.rs index e2c85924..d0893610 100644 --- a/src/testutils/blockchain_tests.rs +++ b/src/testutils/blockchain_tests.rs @@ -145,9 +145,7 @@ impl TestClient { let bumped: serde_json::Value = self.call("bumpfee", &[txid.to_string().into()]).unwrap(); let new_txid = Txid::from_str(&bumped["txid"].as_str().unwrap().to_string()).unwrap(); - - let monitor_script = - tx.vout[0].script_pub_key.addresses.as_ref().unwrap()[0].script_pubkey(); + let monitor_script = Script::from_hex(&mut tx.vout[0].script_pub_key.hex.to_hex()).unwrap(); self.wait_for_tx(new_txid, &monitor_script); debug!("Bumped {}, new txid {}", txid, new_txid); @@ -394,6 +392,9 @@ macro_rules! bdk_blockchain_tests { #[test] fn test_sync_simple() { + use std::ops::Deref; + use crate::database::Database; + let (wallet, descriptors, mut test_client) = init_single_sig(); let tx = testutils! { @@ -402,7 +403,13 @@ macro_rules! bdk_blockchain_tests { println!("{:?}", tx); let txid = test_client.receive(tx); + // the RPC blockchain needs to call `sync()` during initialization to import the + // addresses (see `init_single_sig()`), so we skip this assertion + #[cfg(not(feature = "test-rpc"))] + assert!(wallet.database().deref().get_sync_time().unwrap().is_none(), "initial sync_time not none"); + wallet.sync(noop_progress(), None).unwrap(); + assert!(wallet.database().deref().get_sync_time().unwrap().is_some(), "sync_time hasn't been updated"); assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance"); assert_eq!(wallet.list_unspent().unwrap()[0].keychain, KeychainKind::External, "incorrect keychain kind"); @@ -970,6 +977,102 @@ macro_rules! bdk_blockchain_tests { wallet.sync(noop_progress(), None).unwrap(); assert!(wallet.get_balance().unwrap() > 0, "incorrect balance after receiving coinbase"); } + + #[test] + fn test_send_to_bech32m_addr() { + use std::str::FromStr; + use serde; + use serde_json; + use serde::Serialize; + use bitcoincore_rpc::jsonrpc::serde_json::Value; + use bitcoincore_rpc::{Auth, Client, RpcApi}; + + let (wallet, descriptors, mut test_client) = init_single_sig(); + + // TODO remove once rust-bitcoincore-rpc with PR 199 released + // https://github.com/rust-bitcoin/rust-bitcoincore-rpc/pull/199 + /// Import Descriptor Request + #[derive(Serialize, Clone, PartialEq, Eq, Debug)] + pub struct ImportDescriptorRequest { + pub active: bool, + #[serde(rename = "desc")] + pub descriptor: String, + pub range: [i64; 2], + pub next_index: i64, + pub timestamp: String, + pub internal: bool, + } + + // TODO remove once rust-bitcoincore-rpc with PR 199 released + impl ImportDescriptorRequest { + /// Create a new Import Descriptor request providing just the descriptor and internal flags + pub fn new(descriptor: &str, internal: bool) -> Self { + ImportDescriptorRequest { + descriptor: descriptor.to_string(), + internal, + active: true, + range: [0, 100], + next_index: 0, + timestamp: "now".to_string(), + } + } + } + + // 1. Create and add descriptors to a test bitcoind node taproot wallet + + // TODO replace once rust-bitcoincore-rpc with PR 174 released + // https://github.com/rust-bitcoin/rust-bitcoincore-rpc/pull/174 + let _createwallet_result: Value = test_client.bitcoind.client.call("createwallet", &["taproot_wallet".into(),false.into(),true.into(),serde_json::to_value("").unwrap(), false.into(), true.into()]).unwrap(); + + // TODO replace once bitcoind released with support for rust-bitcoincore-rpc PR 174 + let taproot_wallet_client = Client::new(&test_client.bitcoind.rpc_url_with_wallet("taproot_wallet"), Auth::CookieFile(test_client.bitcoind.params.cookie_file.clone())).unwrap(); + + let wallet_descriptor = "tr(tprv8ZgxMBicQKsPdBtxmEMPnNq58KGusNAimQirKFHqX2yk2D8q1v6pNLiKYVAdzDHy2w3vF4chuGfMvNtzsbTTLVXBcdkCA1rje1JG6oksWv8/86h/1h/0h/0/*)#y283ssmn"; + let change_descriptor = "tr(tprv8ZgxMBicQKsPdBtxmEMPnNq58KGusNAimQirKFHqX2yk2D8q1v6pNLiKYVAdzDHy2w3vF4chuGfMvNtzsbTTLVXBcdkCA1rje1JG6oksWv8/86h/1h/0h/1/*)#47zsd9tt"; + + let tr_descriptors = vec![ + ImportDescriptorRequest::new(wallet_descriptor, false), + ImportDescriptorRequest::new(change_descriptor, false), + ]; + + // TODO replace once rust-bitcoincore-rpc with PR 199 released + let _import_result: Value = taproot_wallet_client.call("importdescriptors", &[serde_json::to_value(tr_descriptors).unwrap()]).unwrap(); + + // 2. Get a new bech32m address from test bitcoind node taproot wallet + + // TODO replace once rust-bitcoincore-rpc with PR 199 released + let node_addr: bitcoin::Address = taproot_wallet_client.call("getnewaddress", &["test address".into(), "bech32m".into()]).unwrap(); + assert_eq!(node_addr, bitcoin::Address::from_str("bcrt1pj5y3f0fu4y7g98k4v63j9n0xvj3lmln0cpwhsjzknm6nt0hr0q7qnzwsy9").unwrap()); + + // 3. Send 50_000 sats from test bitcoind node to test BDK wallet + + test_client.receive(testutils! { + @tx ( (@external descriptors, 0) => 50_000 ) + }); + + wallet.sync(noop_progress(), None).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), 50_000, "wallet has incorrect balance"); + + // 4. Send 25_000 sats from test BDK wallet to test bitcoind node taproot wallet + + let mut builder = wallet.build_tx(); + builder.add_recipient(node_addr.script_pubkey(), 25_000); + let (mut psbt, details) = builder.finish().unwrap(); + let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + assert!(finalized, "wallet cannot finalize transaction"); + let tx = psbt.extract_tx(); + wallet.broadcast(&tx).unwrap(); + wallet.sync(noop_progress(), None).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), details.received, "wallet has incorrect balance after send"); + assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "wallet has incorrect number of txs"); + assert_eq!(wallet.list_unspent().unwrap().len(), 1, "wallet has incorrect number of unspents"); + test_client.generate(1, None); + + // 5. Verify 25_000 sats are received by test bitcoind node taproot wallet + + let taproot_balance = taproot_wallet_client.get_balance(None, None).unwrap(); + assert_eq!(taproot_balance.as_sat(), 25_000, "node has incorrect taproot wallet balance"); + } } }; diff --git a/src/types.rs b/src/types.rs index 3e4d8edf..ac4a2228 100644 --- a/src/types.rs +++ b/src/types.rs @@ -210,7 +210,7 @@ pub struct TransactionDetails { pub fee: Option, /// If the transaction is confirmed, contains height and timestamp of the block containing the /// transaction, unconfirmed transaction contains `None`. - pub confirmation_time: Option, + pub confirmation_time: Option, /// Whether the tx has been verified against the consensus rules /// /// Confirmed txs are considered "verified" by default, while unconfirmed txs are checked to @@ -222,20 +222,26 @@ pub struct TransactionDetails { pub verified: bool, } -/// Block height and timestamp of the block containing the confirmed transaction +/// Block height and timestamp of a block #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)] -pub struct ConfirmationTime { +pub struct BlockTime { /// confirmation block height pub height: u32, /// confirmation block timestamp pub timestamp: u64, } -impl ConfirmationTime { - /// Returns `Some` `ConfirmationTime` if both `height` and `timestamp` are `Some` +/// **DEPRECATED**: Confirmation time of a transaction +/// +/// The structure has been renamed to `BlockTime` +#[deprecated(note = "This structure has been renamed to `BlockTime`")] +pub type ConfirmationTime = BlockTime; + +impl BlockTime { + /// Returns `Some` `BlockTime` if both `height` and `timestamp` are `Some` pub fn new(height: Option, timestamp: Option) -> Option { match (height, timestamp) { - (Some(height), Some(timestamp)) => Some(ConfirmationTime { height, timestamp }), + (Some(height), Some(timestamp)) => Some(BlockTime { height, timestamp }), _ => None, } } diff --git a/src/wallet/export.rs b/src/wallet/export.rs index 047e93a1..e39d178e 100644 --- a/src/wallet/export.rs +++ b/src/wallet/export.rs @@ -212,7 +212,7 @@ mod test { use crate::database::{memory::MemoryDatabase, BatchOperations}; use crate::types::TransactionDetails; use crate::wallet::Wallet; - use crate::ConfirmationTime; + use crate::BlockTime; fn get_test_db() -> MemoryDatabase { let mut db = MemoryDatabase::new(); @@ -226,7 +226,7 @@ mod test { received: 100_000, sent: 0, fee: Some(500), - confirmation_time: Some(ConfirmationTime { + confirmation_time: Some(BlockTime { timestamp: 12345678, height: 5000, }), diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index b8d1196d..832626e3 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -57,7 +57,7 @@ use utils::{check_nlocktime, check_nsequence_rbf, After, Older, SecpCtx, DUST_LI use crate::blockchain::{Blockchain, Progress}; use crate::database::memory::MemoryDatabase; -use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils}; +use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils, SyncTime}; use crate::descriptor::derived::AsDerived; use crate::descriptor::policy::BuildSatisfaction; use crate::descriptor::{ @@ -1447,6 +1447,11 @@ where Ok(()) } + + /// Return an immutable reference to the internal database + pub fn database(&self) -> impl std::ops::Deref + '_ { + self.database.borrow() + } } impl Wallet @@ -1549,6 +1554,15 @@ where } } + let sync_time = SyncTime { + block_time: BlockTime { + height: maybe_await!(self.client.get_height())?, + timestamp: time::get_timestamp(), + }, + }; + debug!("Saving `sync_time` = {:?}", sync_time); + self.database.borrow_mut().set_sync_time(sync_time)?; + Ok(()) } @@ -2778,7 +2792,7 @@ pub(crate) mod test { let txid = tx.txid(); // skip saving the utxos, we know they can't be used anyways details.transaction = Some(tx); - details.confirmation_time = Some(ConfirmationTime { + details.confirmation_time = Some(BlockTime { timestamp: 12345678, height: 42, }); @@ -3980,4 +3994,15 @@ pub(crate) mod test { } ); } + + #[test] + fn test_sending_to_bip350_bech32m_address() { + let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); + let addr = + Address::from_str("tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c") + .unwrap(); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 45_000); + builder.finish().unwrap(); + } }