diff --git a/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/LiveTxBuilderTest.kt b/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/LiveTxBuilderTest.kt index 797451e7..c78d761b 100644 --- a/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/LiveTxBuilderTest.kt +++ b/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/LiveTxBuilderTest.kt @@ -27,7 +27,7 @@ class LiveTxBuilderTest { @Test fun testTxBuilder() { - var conn: Connection = Connection.newInMemory() + var conn: Persister = Persister.newInMemory() val wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET, conn) val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL) val fullScanRequest: FullScanRequest = wallet.startFullScan().build() @@ -51,7 +51,7 @@ class LiveTxBuilderTest { @Test fun complexTxBuilder() { - var conn: Connection = Connection.newInMemory() + var conn: Persister = Persister.newInMemory() val wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET, conn) val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL) val fullScanRequest: FullScanRequest = wallet.startFullScan().build() diff --git a/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/LiveWalletTest.kt b/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/LiveWalletTest.kt index 32a31119..64dbdb60 100644 --- a/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/LiveWalletTest.kt +++ b/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/LiveWalletTest.kt @@ -28,7 +28,7 @@ class LiveWalletTest { @Test fun testSyncedBalance() { - var conn: Connection = Connection.newInMemory() + var conn: Persister = Persister.newInMemory() val wallet: Wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET, conn) val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL) val fullScanRequest: FullScanRequest = wallet.startFullScan().build() @@ -54,7 +54,7 @@ class LiveWalletTest { @Test fun testBroadcastTransaction() { - var conn: Connection = Connection.newInMemory() + var conn: Persister = Persister.newInMemory() val wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET, conn) val esploraClient = EsploraClient(SIGNET_ESPLORA_URL) val fullScanRequest: FullScanRequest = wallet.startFullScan().build() diff --git a/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/OfflineWalletTest.kt b/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/OfflineWalletTest.kt index 77d0dc82..0cd8120f 100644 --- a/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/OfflineWalletTest.kt +++ b/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/OfflineWalletTest.kt @@ -36,7 +36,7 @@ class OfflineWalletTest { @Test fun testNewAddress() { - val conn = Connection.newInMemory() + val conn = Persister.newInMemory() val wallet: Wallet = Wallet( descriptor, changeDescriptor, @@ -58,7 +58,7 @@ class OfflineWalletTest { @Test fun testBalance() { - var conn: Connection = Connection.newInMemory() + var conn: Persister = Persister.newInMemory() val wallet: Wallet = Wallet( descriptor, changeDescriptor, diff --git a/bdk-ffi/src/bdk.udl b/bdk-ffi/src/bdk.udl index dd667a93..0fba7216 100644 --- a/bdk-ffi/src/bdk.udl +++ b/bdk-ffi/src/bdk.udl @@ -197,7 +197,7 @@ interface ParseAmountError { [Error] interface PersistenceError { - Write(string error_message); + Reason(string error_message); }; [Error] diff --git a/bdk-ffi/src/error.rs b/bdk-ffi/src/error.rs index ebd14ec3..afcfc61d 100644 --- a/bdk-ffi/src/error.rs +++ b/bdk-ffi/src/error.rs @@ -564,8 +564,8 @@ pub enum ParseAmountError { #[derive(Debug, thiserror::Error)] pub enum PersistenceError { - #[error("writing to persistence error: {error_message}")] - Write { error_message: String }, + #[error("persistence error: {error_message}")] + Reason { error_message: String }, } #[derive(Debug, thiserror::Error)] @@ -745,12 +745,6 @@ pub enum SignerError { Psbt { error_message: String }, } -#[derive(Debug, thiserror::Error, uniffi::Error)] -pub enum SqliteError { - #[error("sqlite error: {rusqlite_error}")] - Sqlite { rusqlite_error: String }, -} - #[derive(Debug, thiserror::Error)] pub enum TransactionError { #[error("io error")] @@ -1251,6 +1245,14 @@ impl From> for LoadWithPersistEr } } +impl From for PersistenceError { + fn from(error: BdkSqliteError) -> Self { + PersistenceError::Reason { + error_message: error.to_string(), + } + } +} + impl From for MiniscriptError { fn from(error: bdk_wallet::miniscript::Error) -> Self { use bdk_wallet::miniscript::Error as BdkMiniscriptError; @@ -1343,7 +1345,7 @@ impl From for ParseAmountError { impl From for PersistenceError { fn from(error: std::io::Error) -> Self { - PersistenceError::Write { + PersistenceError::Reason { error_message: error.to_string(), } } @@ -1513,14 +1515,6 @@ pub enum HashParseError { InvalidHash { len: u32 }, } -impl From for SqliteError { - fn from(error: BdkSqliteError) -> Self { - SqliteError::Sqlite { - rusqlite_error: error.to_string(), - } - } -} - impl From for CbfBuilderError { fn from(value: bdk_kyoto::builder::SqlInitializationError) -> Self { CbfBuilderError::DatabaseError { @@ -1544,7 +1538,7 @@ mod test { use crate::error::SignerError; use crate::error::{ Bip32Error, Bip39Error, CannotConnectError, DescriptorError, DescriptorKeyError, - ElectrumError, EsploraError, ExtractTxError, PersistenceError, PsbtError, PsbtParseError, + ElectrumError, EsploraError, ExtractTxError, PsbtError, PsbtParseError, RequestBuilderError, TransactionError, TxidParseError, }; @@ -1929,30 +1923,6 @@ mod test { } } - #[test] - fn test_persistence_error() { - let cases = vec![ - ( - std::io::Error::new( - std::io::ErrorKind::Other, - "unable to persist the new address", - ) - .into(), - "writing to persistence error: unable to persist the new address", - ), - ( - PersistenceError::Write { - error_message: "failed to write to storage".to_string(), - }, - "writing to persistence error: failed to write to storage", - ), - ]; - - for (error, expected_message) in cases { - assert_eq!(error.to_string(), expected_message); - } - } - #[test] fn test_error_psbt() { let cases = vec![ diff --git a/bdk-ffi/src/store.rs b/bdk-ffi/src/store.rs index 7df64891..f4484ff1 100644 --- a/bdk-ffi/src/store.rs +++ b/bdk-ffi/src/store.rs @@ -1,34 +1,88 @@ -use crate::error::SqliteError; +use crate::error::PersistenceError; +use crate::types::ChangeSet; -use bdk_wallet::rusqlite::Connection as BdkConnection; +use bdk_wallet::{rusqlite::Connection as BdkConnection, WalletPersister}; -use std::sync::Mutex; -use std::sync::MutexGuard; +use std::ops::DerefMut; +use std::sync::{Arc, Mutex}; -/// A connection to a SQLite database. +/// Definition of a wallet persistence implementation. +#[uniffi::export(with_foreign)] +pub trait Persistence: Send + Sync { + /// Initialize the total aggregate `ChangeSet` for the underlying wallet. + fn initialize(&self) -> Result, PersistenceError>; + + /// Persist a `ChangeSet` to the total aggregate changeset of the wallet. + fn persist(&self, changeset: Arc) -> Result<(), PersistenceError>; +} + +pub(crate) enum PersistenceType { + Custom(Arc), + Sql(Mutex), +} + +/// Wallet backend implementations. #[derive(uniffi::Object)] -pub struct Connection(Mutex); +pub struct Persister { + pub(crate) inner: Mutex, +} #[uniffi::export] -impl Connection { - /// Open a new connection to a SQLite database. If a database does not exist at the path, one is - /// created. +impl Persister { + /// Create a new Sqlite connection at the specified file path. #[uniffi::constructor] - pub fn new(path: String) -> Result { - let connection = BdkConnection::open(path)?; - Ok(Self(Mutex::new(connection))) + pub fn new_sqlite(path: String) -> Result { + let conn = BdkConnection::open(path)?; + Ok(Self { + inner: PersistenceType::Sql(conn.into()).into(), + }) } - /// Open a new connection to an in-memory SQLite database. + /// Create a new connection in memory. #[uniffi::constructor] - pub fn new_in_memory() -> Result { - let connection = BdkConnection::open_in_memory()?; - Ok(Self(Mutex::new(connection))) + pub fn new_in_memory() -> Result { + let conn = BdkConnection::open_in_memory()?; + Ok(Self { + inner: PersistenceType::Sql(conn.into()).into(), + }) + } + + /// Use a native persistence layer. + #[uniffi::constructor] + pub fn custom(persistence: Arc) -> Self { + Self { + inner: PersistenceType::Custom(persistence).into(), + } } } -impl Connection { - pub(crate) fn get_store(&self) -> MutexGuard { - self.0.lock().expect("must lock") +impl WalletPersister for PersistenceType { + type Error = PersistenceError; + + fn initialize(persister: &mut Self) -> Result { + match persister { + PersistenceType::Sql(ref conn) => { + let mut lock = conn.lock().unwrap(); + let deref = lock.deref_mut(); + Ok(BdkConnection::initialize(deref)?) + } + PersistenceType::Custom(any) => any + .initialize() + .map(|changeset| changeset.as_ref().clone().into()), + } + } + + fn persist(persister: &mut Self, changeset: &bdk_wallet::ChangeSet) -> Result<(), Self::Error> { + match persister { + PersistenceType::Sql(ref conn) => { + let mut lock = conn.lock().unwrap(); + let deref = lock.deref_mut(); + Ok(BdkConnection::persist(deref, changeset)?) + } + PersistenceType::Custom(any) => { + let ffi_changeset: ChangeSet = changeset.clone().into(); + any.persist(Arc::new(ffi_changeset)) + } + } } } diff --git a/bdk-ffi/src/tx_builder.rs b/bdk-ffi/src/tx_builder.rs index 8f468650..ee097ed3 100644 --- a/bdk-ffi/src/tx_builder.rs +++ b/bdk-ffi/src/tx_builder.rs @@ -549,7 +549,7 @@ pub enum ChangeSpendPolicy { mod tests { use crate::bitcoin::{Amount, Script}; use crate::{ - descriptor::Descriptor, esplora::EsploraClient, store::Connection, + descriptor::Descriptor, esplora::EsploraClient, store::Persister, types::FullScanScriptInspector, wallet::Wallet, }; use bdk_wallet::bitcoin::Network; @@ -616,7 +616,7 @@ mod tests { Arc::new(Descriptor::new(external_descriptor, Network::Signet).unwrap()), Arc::new(Descriptor::new(internal_descriptor, Network::Signet).unwrap()), Network::Signet, - Arc::new(Connection::new_in_memory().unwrap()), + Arc::new(Persister::new_in_memory().unwrap()), 25, ) .unwrap(); diff --git a/bdk-ffi/src/types.rs b/bdk-ffi/src/types.rs index afb2a972..0c0b9b50 100644 --- a/bdk-ffi/src/types.rs +++ b/bdk-ffi/src/types.rs @@ -7,7 +7,7 @@ use crate::error::{CreateTxError, RequestBuilderError}; use bdk_core::bitcoin::absolute::LockTime as BdkLockTime; use bdk_core::spk_client::SyncItem; -use bdk_core::BlockId as BdkBlockId; +use bdk_core::{BlockId as BdkBlockId, Merge}; use bdk_wallet::bitcoin::Transaction as BdkTransaction; use bdk_wallet::chain::spk_client::FullScanRequest as BdkFullScanRequest; @@ -788,6 +788,12 @@ impl From for bdk_wallet::chain::indexer::keychain_txout::Chan } } +impl Default for IndexerChangeSet { + fn default() -> Self { + bdk_wallet::chain::indexer::keychain_txout::ChangeSet::default().into() + } +} + /// The hash added or removed at the given height. #[derive(Debug, Clone, uniffi::Record)] pub struct ChainChange { @@ -827,6 +833,12 @@ impl From for bdk_wallet::chain::local_chain::ChangeSet { } } +impl Default for LocalChainChangeSet { + fn default() -> Self { + bdk_wallet::chain::local_chain::ChangeSet::default().into() + } +} + #[derive(Debug, Clone, uniffi::Record)] pub struct Anchor { pub confirmation_block_time: ConfirmationBlockTime, @@ -875,6 +887,12 @@ impl From> for } } +impl Default for TxGraphChangeSet { + fn default() -> Self { + bdk_wallet::chain::tx_graph::ChangeSet::default().into() + } +} + impl From for bdk_wallet::chain::tx_graph::ChangeSet { fn from(mut value: TxGraphChangeSet) -> Self { let mut txs = BTreeSet::new(); @@ -904,14 +922,123 @@ impl From for bdk_wallet::chain::tx_graph::ChangeSet>, - pub change_descriptor: Option>, - pub network: Option, - pub local_chain: LocalChainChangeSet, - pub tx_graph: TxGraphChangeSet, - pub indexer: IndexerChangeSet, + descriptor: Option>, + change_descriptor: Option>, + network: Option, + local_chain: LocalChainChangeSet, + tx_graph: TxGraphChangeSet, + indexer: IndexerChangeSet, +} + +#[uniffi::export] +impl ChangeSet { + /// Create an empty `ChangeSet`. + #[uniffi::constructor] + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + bdk_wallet::ChangeSet::default().into() + } + + #[uniffi::constructor] + pub fn from_aggregate( + descriptor: Option>, + change_descriptor: Option>, + network: Option, + local_chain: LocalChainChangeSet, + tx_graph: TxGraphChangeSet, + indexer: IndexerChangeSet, + ) -> Self { + Self { + descriptor, + change_descriptor, + network, + local_chain, + tx_graph, + indexer, + } + } + + #[uniffi::constructor] + pub fn from_descriptor_and_network( + descriptor: Option>, + change_descriptor: Option>, + network: Option, + ) -> Self { + Self { + descriptor, + change_descriptor, + network, + local_chain: LocalChainChangeSet::default(), + tx_graph: TxGraphChangeSet::default(), + indexer: IndexerChangeSet::default(), + } + } + + /// Start a wallet `ChangeSet` from local chain changes. + #[uniffi::constructor] + pub fn from_local_chain_changes(local_chain_changes: LocalChainChangeSet) -> Self { + let local_chain: bdk_wallet::chain::local_chain::ChangeSet = local_chain_changes.into(); + let changeset: bdk_wallet::ChangeSet = local_chain.into(); + changeset.into() + } + + /// Start a wallet `ChangeSet` from indexer changes. + #[uniffi::constructor] + pub fn from_indexer_changeset(indexer_changes: IndexerChangeSet) -> Self { + let indexer: bdk_wallet::chain::indexer::keychain_txout::ChangeSet = indexer_changes.into(); + let changeset: bdk_wallet::ChangeSet = indexer.into(); + changeset.into() + } + + /// Start a wallet `ChangeSet` from transaction graph changes. + #[uniffi::constructor] + pub fn from_tx_graph_changeset(tx_graph_changeset: TxGraphChangeSet) -> Self { + let tx_graph: bdk_wallet::chain::tx_graph::ChangeSet = + tx_graph_changeset.into(); + let changeset: bdk_wallet::ChangeSet = tx_graph.into(); + changeset.into() + } + + /// Build a `ChangeSet` by merging together two `ChangeSet`. + #[uniffi::constructor] + pub fn from_merge(left: Arc, right: Arc) -> Self { + let mut left: bdk_wallet::ChangeSet = left.as_ref().clone().into(); + let right: bdk_wallet::ChangeSet = right.as_ref().clone().into(); + left.merge(right); + left.into() + } + + /// Get the receiving `Descriptor`. + pub fn descriptor(&self) -> Option> { + self.descriptor.clone() + } + + /// Get the change `Descriptor` + pub fn change_descriptor(&self) -> Option> { + self.change_descriptor.clone() + } + + /// Get the `Network` + pub fn network(&self) -> Option { + self.network + } + + /// Get the changes to the local chain. + pub fn localchain_changeset(&self) -> LocalChainChangeSet { + self.local_chain.clone() + } + + /// Get the changes to the transaction graph. + pub fn tx_graph_changeset(&self) -> TxGraphChangeSet { + self.tx_graph.clone() + } + + /// Get the changes to the indexer. + pub fn indexer_changeset(&self) -> IndexerChangeSet { + self.indexer.clone() + } } impl From for bdk_wallet::ChangeSet { diff --git a/bdk-ffi/src/wallet.rs b/bdk-ffi/src/wallet.rs index e94abad3..40aa694b 100644 --- a/bdk-ffi/src/wallet.rs +++ b/bdk-ffi/src/wallet.rs @@ -2,9 +2,9 @@ use crate::bitcoin::{Amount, FeeRate, OutPoint, Psbt, Script, Transaction, Txid} use crate::descriptor::Descriptor; use crate::error::{ CalculateFeeError, CannotConnectError, CreateWithPersistError, DescriptorError, - LoadWithPersistError, SignerError, SqliteError, TxidParseError, + LoadWithPersistError, PersistenceError, SignerError, TxidParseError, }; -use crate::store::Connection; +use crate::store::{PersistenceType, Persister}; use crate::types::{ AddressInfo, Balance, BlockId, CanonicalTx, FullScanRequestBuilder, KeychainAndIndex, LocalOutput, Policy, SentAndReceivedValues, SignOptions, SyncRequestBuilder, UnconfirmedTx, @@ -12,11 +12,10 @@ use crate::types::{ }; use bdk_wallet::bitcoin::Network; -use bdk_wallet::rusqlite::Connection as BdkConnection; use bdk_wallet::signer::SignOptions as BdkSignOptions; use bdk_wallet::{KeychainKind, PersistedWallet, Wallet as BdkWallet}; -use std::borrow::BorrowMut; +use std::ops::DerefMut; use std::sync::{Arc, Mutex, MutexGuard}; /// A Bitcoin wallet. @@ -33,7 +32,7 @@ use std::sync::{Arc, Mutex, MutexGuard}; /// script pubkeys. See KeychainTxOutIndex::insert_descriptor() for more details. #[derive(uniffi::Object)] pub struct Wallet { - inner_mutex: Mutex>, + inner_mutex: Mutex>, } #[uniffi::export] @@ -46,17 +45,23 @@ impl Wallet { descriptor: Arc, change_descriptor: Arc, network: Network, - connection: Arc, + persister: Arc, lookahead: u32, ) -> Result { let descriptor = descriptor.to_string_with_secret(); let change_descriptor = change_descriptor.to_string_with_secret(); - let mut binding = connection.get_store(); - let db: &mut BdkConnection = binding.borrow_mut(); - let params = BdkWallet::create(descriptor, change_descriptor) - .network(network) - .lookahead(lookahead); - let wallet: PersistedWallet = params.create_wallet(db)?; + let mut persist_lock = persister.inner.lock().unwrap(); + let deref = persist_lock.deref_mut(); + + let wallet: PersistedWallet = + BdkWallet::create(descriptor, change_descriptor) + .network(network) + .lookahead(lookahead) + .create_wallet(deref) + .map_err(|e| CreateWithPersistError::Persist { + error_message: e.to_string(), + })?; + Ok(Wallet { inner_mutex: Mutex::new(wallet), }) @@ -69,18 +74,21 @@ impl Wallet { pub fn load( descriptor: Arc, change_descriptor: Arc, - connection: Arc, + persister: Arc, ) -> Result { let descriptor = descriptor.to_string_with_secret(); let change_descriptor = change_descriptor.to_string_with_secret(); - let mut binding = connection.get_store(); - let db: &mut BdkConnection = binding.borrow_mut(); + let mut persist_lock = persister.inner.lock().unwrap(); + let deref = persist_lock.deref_mut(); - let wallet: PersistedWallet = BdkWallet::load() + let wallet: PersistedWallet = BdkWallet::load() .descriptor(KeychainKind::External, Some(descriptor)) .descriptor(KeychainKind::Internal, Some(change_descriptor)) .extract_keys() - .load_wallet(db)? + .load_wallet(deref) + .map_err(|e| LoadWithPersistError::Persist { + error_message: e.to_string(), + })? .ok_or(LoadWithPersistError::CouldNotLoad)?; Ok(Wallet { @@ -404,13 +412,13 @@ impl Wallet { /// Returns whether any new changes were persisted. /// /// If the persister errors, the staged changes will not be cleared. - pub fn persist(&self, connection: Arc) -> Result { - let mut binding = connection.get_store(); - let db: &mut BdkConnection = binding.borrow_mut(); + pub fn persist(&self, persister: Arc) -> Result { + let mut persist_lock = persister.inner.lock().unwrap(); + let deref = persist_lock.deref_mut(); self.get_wallet() - .persist(db) - .map_err(|e| SqliteError::Sqlite { - rusqlite_error: e.to_string(), + .persist(deref) + .map_err(|e| PersistenceError::Reason { + error_message: e.to_string(), }) } @@ -421,7 +429,7 @@ impl Wallet { } impl Wallet { - pub(crate) fn get_wallet(&self) -> MutexGuard> { + pub(crate) fn get_wallet(&self) -> MutexGuard> { self.inner_mutex.lock().expect("wallet") } } diff --git a/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveElectrumClientTest.kt b/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveElectrumClientTest.kt index ea282f04..781c3730 100644 --- a/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveElectrumClientTest.kt +++ b/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveElectrumClientTest.kt @@ -18,7 +18,7 @@ class LiveElectrumClientTest { @Test fun testSyncedBalance() { - var conn: Connection = Connection.newInMemory() + var conn: Persister = Persister.newInMemory() val wallet: Wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET, conn) val electrumClient: ElectrumClient = ElectrumClient(SIGNET_ELECTRUM_URL) val fullScanRequest: FullScanRequest = wallet.startFullScan().build() diff --git a/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveKyotoTest.kt b/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveKyotoTest.kt index dc65a719..2a298553 100644 --- a/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveKyotoTest.kt +++ b/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveKyotoTest.kt @@ -28,7 +28,7 @@ class LiveKyotoTest { @Test fun testKyoto() { - val conn: Connection = Connection.newInMemory() + val conn: Persister = Persister.newInMemory() val wallet: Wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET, conn) val peers = listOf(peer) runBlocking { diff --git a/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveMemoryWalletTest.kt b/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveMemoryWalletTest.kt index 1b7c9246..e640077b 100644 --- a/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveMemoryWalletTest.kt +++ b/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveMemoryWalletTest.kt @@ -21,7 +21,7 @@ class LiveMemoryWalletTest { "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/0/*)", Network.SIGNET ) - var conn: Connection = Connection.newInMemory() + var conn: Persister = Persister.newInMemory() val wallet: Wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET, conn) val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL) val fullScanRequest: FullScanRequest = wallet.startFullScan().build() @@ -45,7 +45,7 @@ class LiveMemoryWalletTest { @Test fun testScriptInspector() { - var conn: Connection = Connection.newInMemory() + var conn: Persister = Persister.newInMemory() val wallet: Wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET, conn) val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL) diff --git a/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveTransactionTest.kt b/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveTransactionTest.kt index 58e688d8..f8dbc3fa 100644 --- a/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveTransactionTest.kt +++ b/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveTransactionTest.kt @@ -17,7 +17,7 @@ class LiveTransactionTest { @Test fun testSyncedBalance() { - var conn: Connection = Connection.newInMemory() + var conn: Persister = Persister.newInMemory() val wallet: Wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET, conn) val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL) val fullScanRequest: FullScanRequest = wallet.startFullScan().build() diff --git a/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveTxBuilderTest.kt b/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveTxBuilderTest.kt index f1bcb7e9..6debbd5b 100644 --- a/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveTxBuilderTest.kt +++ b/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveTxBuilderTest.kt @@ -32,7 +32,7 @@ class LiveTxBuilderTest { @Test fun testTxBuilder() { - val connection: Connection = Connection(persistenceFilePath) + val connection: Persister = Persister.newSqlite(persistenceFilePath) val wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET, connection) val esploraClient = EsploraClient(SIGNET_ESPLORA_URL) val fullScanRequest: FullScanRequest = wallet.startFullScan().build() @@ -58,7 +58,7 @@ class LiveTxBuilderTest { @Test fun complexTxBuilder() { - val connection: Connection = Connection(persistenceFilePath) + val connection: Persister = Persister.newSqlite(persistenceFilePath) val wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET, connection) val esploraClient = EsploraClient(SIGNET_ESPLORA_URL) val fullScanRequest: FullScanRequest = wallet.startFullScan().build() diff --git a/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveWalletTest.kt b/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveWalletTest.kt index 3f44d0bf..59d8fce3 100644 --- a/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveWalletTest.kt +++ b/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveWalletTest.kt @@ -32,7 +32,7 @@ class LiveWalletTest { @Test fun testSyncedBalance() { - val connection = Connection(persistenceFilePath) + val connection = Persister.newSqlite(persistenceFilePath) val wallet: Wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET, connection) val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL) val fullScanRequest: FullScanRequest = wallet.startFullScan().build() @@ -57,7 +57,7 @@ class LiveWalletTest { @Test fun testBroadcastTransaction() { - val connection = Connection(persistenceFilePath) + val connection = Persister.newSqlite(persistenceFilePath) val wallet: Wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET, connection) val esploraClient = EsploraClient(SIGNET_ESPLORA_URL) val fullScanRequest: FullScanRequest = wallet.startFullScan().build() diff --git a/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/OfflinePersistenceTest.kt b/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/OfflinePersistenceTest.kt index 5d2db4f1..38981e03 100644 --- a/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/OfflinePersistenceTest.kt +++ b/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/OfflinePersistenceTest.kt @@ -20,7 +20,7 @@ class OfflinePersistenceTest { @Test fun testPersistence() { - val connection = Connection(persistenceFilePath) + val connection = Persister.newSqlite(persistenceFilePath) val wallet: Wallet = Wallet.load( descriptor, @@ -42,7 +42,7 @@ class OfflinePersistenceTest { @Test fun testPersistenceWithDescriptor() { - val connection = Connection(persistenceFilePath) + val connection = Persister.newSqlite(persistenceFilePath) val descriptorPub = Descriptor( "wpkh([9122d9e0/84'/1'/0']tpubDCYVtmaSaDzTxcgvoP5AHZNbZKZzrvoNH9KARep88vESc6MxRqAp4LmePc2eeGX6XUxBcdhAmkthWTDqygPz2wLAyHWisD299Lkdrj5egY6/0/*)#zpaanzgu", diff --git a/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/OfflineWalletTest.kt b/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/OfflineWalletTest.kt index b85a3bc7..b5b5edb1 100644 --- a/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/OfflineWalletTest.kt +++ b/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/OfflineWalletTest.kt @@ -28,7 +28,7 @@ class OfflineWalletTest { @Test fun testNewAddress() { - var conn: Connection = Connection.newInMemory() + var conn: Persister = Persister.newInMemory() val wallet: Wallet = Wallet( descriptor, changeDescriptor, @@ -50,7 +50,7 @@ class OfflineWalletTest { @Test fun testBalance() { - var conn: Connection = Connection.newInMemory() + var conn: Persister = Persister.newInMemory() val wallet: Wallet = Wallet( descriptor, changeDescriptor, diff --git a/bdk-python/tests/test_live_kyoto.py b/bdk-python/tests/test_live_kyoto.py index 04e82b29..651c7148 100644 --- a/bdk-python/tests/test_live_kyoto.py +++ b/bdk-python/tests/test_live_kyoto.py @@ -1,4 +1,4 @@ -from bdkpython import Connection, Network, Descriptor, KeychainKind, CbfBuilder, CbfComponents, CbfClient, CbfNode, CbfError, IpAddress, ScanType, Peer, Update, Wallet +from bdkpython import Persister, Network, Descriptor, KeychainKind, CbfBuilder, CbfComponents, CbfClient, CbfNode, CbfError, IpAddress, ScanType, Peer, Update, Wallet import unittest import os import asyncio @@ -28,12 +28,12 @@ def tearDown(self) -> None: os.remove("./data/signet/peers.db") async def testKyoto(self) -> None: - connection: Connection = Connection.new_in_memory() + persister: Persister = Persister.new_in_memory() wallet: Wallet = Wallet( descriptor, change_descriptor, network, - connection + persister ) peers = [peer] light_client: CbfComponents = CbfBuilder().scan_type(ScanType.NEW()).peers(peers).connections(1).build(wallet) diff --git a/bdk-python/tests/test_live_tx_builder.py b/bdk-python/tests/test_live_tx_builder.py index ffc14edf..aab9f48e 100644 --- a/bdk-python/tests/test_live_tx_builder.py +++ b/bdk-python/tests/test_live_tx_builder.py @@ -7,7 +7,7 @@ from bdkpython import Address from bdkpython import Psbt from bdkpython import TxBuilder -from bdkpython import Connection +from bdkpython import Persister from bdkpython import Network from bdkpython import Amount from bdkpython import FeeRate @@ -34,7 +34,7 @@ def tearDown(self) -> None: os.remove("./bdk_persistence.sqlite") def test_tx_builder(self): - connection: Connection = Connection.new_in_memory() + connection: Persister = Persister.new_in_memory() wallet: Wallet = Wallet( descriptor, change_descriptor, @@ -66,12 +66,12 @@ def test_tx_builder(self): self.assertTrue(psbt.serialize().startswith("cHNi"), "The PSBT should start with cHNi") def complex_tx_builder(self): - connection: Connection = Connection.new_in_memory() + persister: Persister = Persister.new_in_memory() wallet: Wallet = Wallet( descriptor, change_descriptor, Network.SIGNET, - connection + persister ) esplora_client: EsploraClient = EsploraClient(url = SIGNET_ESPLORA_URL) full_scan_request: FullScanRequest = wallet.start_full_scan().build() diff --git a/bdk-python/tests/test_live_wallet.py b/bdk-python/tests/test_live_wallet.py index a4dcc996..f4f5cf9d 100644 --- a/bdk-python/tests/test_live_wallet.py +++ b/bdk-python/tests/test_live_wallet.py @@ -6,7 +6,7 @@ from bdkpython import Address from bdkpython import Psbt from bdkpython import TxBuilder -from bdkpython import Connection +from bdkpython import Persister from bdkpython import Network from bdkpython import Amount from bdkpython import FeeRate @@ -33,12 +33,12 @@ def tearDown(self) -> None: os.remove("./bdk_persistence.sqlite") def test_synced_balance(self): - connection: Connection = Connection.new_in_memory() + persister: Persister = Persister.new_in_memory() wallet: Wallet = Wallet( descriptor, change_descriptor, Network.SIGNET, - connection + persister ) esplora_client: EsploraClient = EsploraClient(url = SIGNET_ESPLORA_URL) full_scan_request: FullScanRequest = wallet.start_full_scan().build() @@ -65,12 +65,12 @@ def test_synced_balance(self): def test_broadcast_transaction(self): - connection: Connection = Connection.new_in_memory() + persister: Persister = Persister.new_in_memory() wallet: Wallet = Wallet( descriptor, change_descriptor, Network.SIGNET, - connection + persister ) esplora_client: EsploraClient = EsploraClient(url = SIGNET_ESPLORA_URL) full_scan_request: FullScanRequest = wallet.start_full_scan().build() @@ -108,4 +108,4 @@ def test_broadcast_transaction(self): if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() diff --git a/bdk-python/tests/test_offline_custom_persist.py b/bdk-python/tests/test_offline_custom_persist.py new file mode 100644 index 00000000..68ee9b6a --- /dev/null +++ b/bdk-python/tests/test_offline_custom_persist.py @@ -0,0 +1,329 @@ + +import binascii +import json +import unittest +from typing import Any, Dict, List, Optional + +import bdkpython as bdk + +initial_txs = [ + "0200000000010101d7eb881ab8cac7d6adc6a7f9aa13e694813d95330c7299cee3623e5d14bd590000000000fdffffff02c5e6c1010000000016001407103a1cccf6a1ea654bee964a4020d20c41fb055c819725010000001600146337ec04bf42015e5d077b90cae05c06925c491a0247304402206aae2bf32da4c3b71cb95e6633c22f9f5a4a4f459975965c0c39b0ab439737b702200c4b16d2029383190965b07adeb87e6d634c68c70d2742f25e456874e8dc273a012103930326d6d72f8663340ce4341d0d3bdb1a1c0734d46e5df8a3003ab6bb50073b00000000", + "02000000000101b0db431cffebeeeeec19ee8a09a2ae4755722ea73232dbb99b8e754eaad6ac300100000000fdffffff024ad24201000000001600146a7b71a68b261b0b7c79e5bb00f0f3d65d5ae4a285ae542401000000160014e43ff61232ca20061ef1d241e73f322a149a23d902473044022059f4b2fa8b9da34dbb57e491f3d5b8a47a623d7e6ebc1b6adfe6d2be744c9640022073cfc8311c49a8d48d69076466d32be591d3c0092b965828cfbcaca69fd409c90121027aa62d03db46272fa31bc1a6cb095bb66bc5409dd74b25e88e3099d84a17a3e469000000", +] +descriptor: bdk.Descriptor = bdk.Descriptor( + "wpkh([44250c36/84'/1'/0']tpubDCrUjjHLB1fxk1oRveETjw62z8jsUuqx7JkBUW44VBszGmcY3Eun3apwVcE5X2bfF5MsM3uvuQDed6Do33ZN8GiWcnj2QPqVDspFT1AyZJ9/0/*)", + bdk.Network.REGTEST, +) +change_descriptor: bdk.Descriptor = bdk.Descriptor( + "wpkh([44250c36/84'/1'/0']tpubDCrUjjHLB1fxk1oRveETjw62z8jsUuqx7JkBUW44VBszGmcY3Eun3apwVcE5X2bfF5MsM3uvuQDed6Do33ZN8GiWcnj2QPqVDspFT1AyZJ9/1/*)", + bdk.Network.REGTEST, +) + + +serialized_persistence = """{"descriptor": "wpkh([44250c36/84'/1'/0']tpubDCrUjjHLB1fxk1oRveETjw62z8jsUuqx7JkBUW44VBszGmcY3Eun3apwVcE5X2bfF5MsM3uvuQDed6Do33ZN8GiWcnj2QPqVDspFT1AyZJ9/0/*)#9q4e992d", "change_descriptor": "wpkh([44250c36/84'/1'/0']tpubDCrUjjHLB1fxk1oRveETjw62z8jsUuqx7JkBUW44VBszGmcY3Eun3apwVcE5X2bfF5MsM3uvuQDed6Do33ZN8GiWcnj2QPqVDspFT1AyZJ9/1/*)#55sccs64", "network": "REGTEST", "local_chain": {"changes": [{"height": 0, "hash": "06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f"}]}, "tx_graph": {"txs": ["0200000000010101d7eb881ab8cac7d6adc6a7f9aa13e694813d95330c7299cee3623e5d14bd590000000000fdffffff02c5e6c1010000000016001407103a1cccf6a1ea654bee964a4020d20c41fb055c819725010000001600146337ec04bf42015e5d077b90cae05c06925c491a0247304402206aae2bf32da4c3b71cb95e6633c22f9f5a4a4f459975965c0c39b0ab439737b702200c4b16d2029383190965b07adeb87e6d634c68c70d2742f25e456874e8dc273a012103930326d6d72f8663340ce4341d0d3bdb1a1c0734d46e5df8a3003ab6bb50073b00000000", "02000000000101b0db431cffebeeeeec19ee8a09a2ae4755722ea73232dbb99b8e754eaad6ac300100000000fdffffff024ad24201000000001600146a7b71a68b261b0b7c79e5bb00f0f3d65d5ae4a285ae542401000000160014e43ff61232ca20061ef1d241e73f322a149a23d902473044022059f4b2fa8b9da34dbb57e491f3d5b8a47a623d7e6ebc1b6adfe6d2be744c9640022073cfc8311c49a8d48d69076466d32be591d3c0092b965828cfbcaca69fd409c90121027aa62d03db46272fa31bc1a6cb095bb66bc5409dd74b25e88e3099d84a17a3e469000000"], "txouts": {}, "anchors": [], "last_seen": {"2d2f7cedc21b4272bf57e3eaaeec241959d15bfa7b710ae984ec1ef2b804c1c0": 0, "b0db431cffebeeeeec19ee8a09a2ae4755722ea73232dbb99b8e754eaad6ac30": 0}}, "indexer": {"last_revealed": {"d29ab90c8fe23b5f43f94462e9128ae15368e83d628a466108d64a08c4abd41f": 8}}}""" + + +class ChangeSetConverter: + @staticmethod + def to_dict(changeset: bdk.ChangeSet) -> Dict: + """ + Serialize a bdk.ChangeSet into a JSON string. + """ + + def _serialize_descriptor(descriptor: Optional[bdk.Descriptor]) -> Optional[str]: + if descriptor is None: + return None + return str(descriptor) + + def _serialize_blockhash(bh: Optional[bdk.BlockHash]) -> Optional[str]: + if bh is None: + return None + return bh.serialize().hex() + + def _serialize_chainchange(cc: bdk.ChainChange) -> Dict[str, Any]: + return {"height": cc.height, "hash": _serialize_blockhash(cc.hash)} + + def _serialize_local_chain(local_chain: bdk.LocalChainChangeSet) -> Dict[str, Any]: + return {"changes": [_serialize_chainchange(cc) for cc in local_chain.changes]} + + def _serialize_tx(tx: bdk.Transaction) -> str: + return tx.serialize().hex() + + def _serialize_outpoint(hop: bdk.HashableOutPoint) -> Dict[str, Any]: + op = hop.outpoint() + txid_hex = op.txid.serialize().hex() + return {"txid": txid_hex, "vout": op.vout} + + def _serialize_txout(txout: bdk.TxOut) -> Dict[str, Any]: + # TxOut.script_pubkey is a bdk.Script instance + script_obj: bdk.Script = txout.script_pubkey + script_bytes = script_obj.to_bytes() + return {"value": txout.value, "script_pubkey": script_bytes.hex()} + + def _serialize_tx_graph(tx_graph: bdk.TxGraphChangeSet) -> Dict[str, Any]: + txs_list = [_serialize_tx(tx) for tx in tx_graph.txs] + + txouts_dict: Dict[str, Dict[str, Any]] = {} + for hop, txout in sorted(tx_graph.txouts.items()): + key = json.dumps(_serialize_outpoint(hop)) + txouts_dict[key] = _serialize_txout(txout) + + anchors_list: List[Dict[str, Any]] = [] + for anchor in tx_graph.anchors: + cbt = anchor.confirmation_block_time + # Serialize BlockId inside ConfirmationBlockTime + block_id = cbt.block_id + bh_hex = _serialize_blockhash(block_id.hash) + block_id_obj = {"height": block_id.height, "hash": bh_hex} + cbt_obj = {"block_id": block_id_obj, "confirmation_time": cbt.confirmation_time} + try: + txid_hex = anchor.txid.serialize().hex() + except AttributeError: + txid_hex = str(anchor.txid) + anchors_list.append({"confirmation_block_time": cbt_obj, "txid": txid_hex}) + + def sort_key(t): + txid_obj , height =t + return txid_obj.serialize().hex() + + last_seen_dict: Dict[str, int] = {} + for txid_obj, height in sorted(tx_graph.last_seen.items(), key=sort_key): + try: + txid_hex = txid_obj.serialize().hex() + except AttributeError: + txid_hex = str(txid_obj) + last_seen_dict[txid_hex] = height + + return { + "txs": txs_list, + "txouts": txouts_dict, + "anchors": anchors_list, + "last_seen": last_seen_dict, + } + + def _serialize_indexer(indexer: bdk.IndexerChangeSet) -> Dict[str, Any]: + lr: Dict[str, int] = {} + for did_obj, idx in sorted(indexer.last_revealed.items()): + did_hex = did_obj.serialize().hex() + lr[did_hex] = idx + return {"last_revealed": lr} + + out: Dict[str, Any] = {} + out["descriptor"] = _serialize_descriptor(changeset.descriptor()) + out["change_descriptor"] = _serialize_descriptor(changeset.change_descriptor()) + + network = changeset.network() + if network is None: + out["network"] = None + else: + out["network"] = network.name + + out["local_chain"] = _serialize_local_chain(changeset.localchain_changeset()) + out["tx_graph"] = _serialize_tx_graph(changeset.tx_graph_changeset()) + out["indexer"] = _serialize_indexer(changeset.indexer_changeset()) + + return out + + @staticmethod + def from_dict(parsed_json: Dict) -> bdk.ChangeSet: + """ + Deserialize a JSON string back into a bdk.ChangeSet. + """ + + def _deserialize_descriptor( + descriptor_str: Optional[str], network: Optional[bdk.Network] + ) -> Optional[bdk.Descriptor]: + if descriptor_str is None: + return None + return bdk.Descriptor(descriptor_str, network) + + def _deserialize_blockhash(hexstr: Optional[str]) -> Optional[bdk.BlockHash]: + if hexstr is None: + return None + raw = binascii.unhexlify(hexstr) + return bdk.BlockHash.from_bytes(raw) + + def _deserialize_chainchange(data: Dict[str, Any]) -> bdk.ChainChange: + height = data["height"] + hash_hex = data["hash"] + bh = _deserialize_blockhash(hash_hex) + return bdk.ChainChange(height=height, hash=bh) + + def _deserialize_local_chain(data: Dict[str, Any]) -> bdk.LocalChainChangeSet: + changes_list = data.get("changes", []) + cc_objs: List[bdk.ChainChange] = [_deserialize_chainchange(cc) for cc in changes_list] + return bdk.LocalChainChangeSet(changes=cc_objs) + + def _deserialize_tx(hexstr: str) -> bdk.Transaction: + raw = binascii.unhexlify(hexstr) + return bdk.Transaction(raw) + + def _deserialize_outpoint(key_str: str) -> bdk.HashableOutPoint: + obj = json.loads(key_str) + txid_hex = obj["txid"] + vout = obj["vout"] + try: + txid_bytes = binascii.unhexlify(txid_hex) + txid_obj = bdk.Txid.from_bytes(txid_bytes) + except Exception: + txid_obj = bdk.Txid(txid_hex) + outpoint = bdk.OutPoint(txid=txid_obj, vout=vout) + return bdk.HashableOutPoint(outpoint=outpoint) + + def _deserialize_txout(data: Dict[str, Any]) -> bdk.TxOut: + value = data["value"] + script_hex = data["script_pubkey"] + script_bytes = binascii.unhexlify(script_hex) + script_obj = bdk.Script(raw_output_script=script_bytes) + return bdk.TxOut(value=value, script_pubkey=script_obj) + + def _deserialize_tx_graph(data: Dict[str, Any]) -> bdk.TxGraphChangeSet: + tx_hex_list = data.get("txs", []) + tx_objs: List[bdk.Transaction] = [_deserialize_tx(h) for h in tx_hex_list] + + txouts_data = data.get("txouts", {}) + txouts_dict: Dict[bdk.HashableOutPoint, bdk.TxOut] = {} + for key_str, txout_data in sorted(txouts_data.items()): + hop = _deserialize_outpoint(key_str) + txout_obj = _deserialize_txout(txout_data) + txouts_dict[hop] = txout_obj + + anchors_list: List[bdk.Anchor] = [] + for anc in data.get("anchors", []): + cbt_data = anc["confirmation_block_time"] + block_id_data = cbt_data["block_id"] + height = block_id_data["height"] + hash_hex = block_id_data["hash"] + bh = _deserialize_blockhash(hash_hex) + block_id_obj = bdk.BlockId(height=height, hash=bh) + + confirmation_time = cbt_data["confirmation_time"] + cbt_obj = bdk.ConfirmationBlockTime( + block_id=block_id_obj, confirmation_time=confirmation_time + ) + + txid_hex = anc["txid"] + try: + txid_bytes = binascii.unhexlify(txid_hex) + txid_obj = bdk.Txid.from_bytes(txid_bytes) + except Exception: + txid_obj = bdk.Txid(txid_hex) + + anchors_list.append(bdk.Anchor(confirmation_block_time=cbt_obj, txid=txid_obj)) + + last_seen_data = data.get("last_seen", {}) + last_seen_dict: Dict[bdk.Txid, int] = {} + for txid_hex, height in sorted(last_seen_data.items()): + try: + txid_obj = bdk.Txid.from_bytes(binascii.unhexlify(txid_hex)) + except Exception: + txid_obj = bdk.Txid(txid_hex) + last_seen_dict[txid_obj] = height + + return bdk.TxGraphChangeSet( + txs=tx_objs, txouts=txouts_dict, anchors=anchors_list, last_seen=last_seen_dict + ) + + def _deserialize_indexer(data: Dict[str, Any]) -> bdk.IndexerChangeSet: + lr_data = data.get("last_revealed", {}) + lr_dict: Dict[bdk.DescriptorId, int] = {} + for did_hex, idx in sorted(lr_data.items()): + did_bytes = binascii.unhexlify(did_hex) + did_obj = bdk.DescriptorId.from_bytes(did_bytes) + lr_dict[did_obj] = idx + return bdk.IndexerChangeSet(last_revealed=lr_dict) + + net = parsed_json.get("network") + if net is None: + network_obj = None + else: + network_obj = getattr(bdk.Network, net) + + descr = _deserialize_descriptor(parsed_json.get("descriptor"), network_obj) + change_descr = _deserialize_descriptor(parsed_json.get("change_descriptor"), network_obj) + local_chain_obj = _deserialize_local_chain(parsed_json["local_chain"]) + tx_graph_obj = _deserialize_tx_graph(parsed_json["tx_graph"]) + indexer_obj = _deserialize_indexer(parsed_json["indexer"]) + + changeset = bdk.ChangeSet.from_descriptor_and_network( + descriptor=descr, + change_descriptor=change_descr, + network=network_obj, + ) + changeset = bdk.ChangeSet.from_merge( + changeset, bdk.ChangeSet.from_local_chain_changes(local_chain_changes=local_chain_obj) + ) + changeset = bdk.ChangeSet.from_merge( + changeset, bdk.ChangeSet.from_tx_graph_changeset(tx_graph_changeset=tx_graph_obj) + ) + changeset = bdk.ChangeSet.from_merge( + changeset, bdk.ChangeSet.from_indexer_changeset(indexer_changes=indexer_obj) + ) + return changeset + + +class MyPersistence(bdk.Persistence): + def __init__(self): + self.memory = [] + + def merge_all(self) -> bdk.ChangeSet: + total_changeset = bdk.ChangeSet() + for changeset_dict in self.memory: + total_changeset = bdk.ChangeSet.from_merge(total_changeset, changeset_dict) + return total_changeset + + def initialize(self) -> bdk.ChangeSet: + return self.merge_all() + + def persist(self, changeset: bdk.ChangeSet): + self.memory.append(changeset) + + +class PersistenceTest(unittest.TestCase): + + def test_synced_transactions(self): + + myp = MyPersistence() + persister = bdk.Persister.custom(myp) + + wallet: bdk.Wallet = bdk.Wallet(descriptor, change_descriptor, bdk.Network.REGTEST, persister) + + wallet.apply_unconfirmed_txs( + [bdk.UnconfirmedTx(tx=bdk.Transaction(bytes.fromhex(tx)), last_seen=0) for tx in initial_txs] + ) + + wallet.persist(persister=persister) + + # initialize new wallet with memory of myp + myp2 = MyPersistence() + myp2.memory = [ChangeSetConverter.from_dict(json.loads(serialized_persistence))] + persister2 = bdk.Persister.custom(myp2) + + wallet2 = bdk.Wallet.load( + descriptor=descriptor, + change_descriptor=change_descriptor, + persister=persister2, + ) + + # check for equality + outputs = wallet.list_output() + outputs2 = wallet2.list_output() + assert len(outputs) == len(outputs2) + for o, o2 in zip(outputs, outputs2): + assert o.outpoint.txid == o2.outpoint.txid + assert o.outpoint.vout == o2.outpoint.vout + + txs = wallet.transactions() + txs2 = wallet2.transactions() + assert txs, "Sync error" + assert len(txs) == len(txs2) + for tx, tx2 in zip(txs, txs2): + assert tx.transaction.compute_txid().serialize() == tx2.transaction.compute_txid().serialize() + + assert wallet.balance().total.to_sat() == 50641167 + d_myp = ChangeSetConverter.to_dict(myp.initialize()) + d_myp2 = ChangeSetConverter.to_dict(myp2.initialize()) + assert json.dumps(d_myp) == json.dumps(d_myp2) + +if __name__ == "__main__": + unittest.main() diff --git a/bdk-python/tests/test_offline_wallet.py b/bdk-python/tests/test_offline_wallet.py index b7b570b5..05c15f00 100644 --- a/bdk-python/tests/test_offline_wallet.py +++ b/bdk-python/tests/test_offline_wallet.py @@ -1,7 +1,7 @@ from bdkpython import Descriptor from bdkpython import Wallet from bdkpython import KeychainKind -from bdkpython import Connection +from bdkpython import Persister from bdkpython import AddressInfo from bdkpython import Network @@ -24,12 +24,12 @@ def tearDown(self) -> None: os.remove("./bdk_persistence.sqlite") def test_new_address(self): - connection: Connection = Connection.new_in_memory() + persister: Persister = Persister.new_in_memory() wallet: Wallet = Wallet( descriptor, change_descriptor, Network.TESTNET, - connection + persister ) address_info: AddressInfo = wallet.reveal_next_address(KeychainKind.EXTERNAL) @@ -41,12 +41,12 @@ def test_new_address(self): self.assertEqual("tb1qhjys9wxlfykmte7ftryptx975uqgd6kcm6a7z4", address_info.address.__str__()) def test_balance(self): - connection: Connection = Connection.new_in_memory() + persister: Persister = Persister.new_in_memory() wallet: Wallet = Wallet( descriptor, change_descriptor, Network.TESTNET, - connection + persister ) self.assertEqual(wallet.balance().total.to_sat(), 0) diff --git a/bdk-swift/Tests/BitcoinDevKitTests/LiveElectrumClientTests.swift b/bdk-swift/Tests/BitcoinDevKitTests/LiveElectrumClientTests.swift index af543fce..4021150d 100644 --- a/bdk-swift/Tests/BitcoinDevKitTests/LiveElectrumClientTests.swift +++ b/bdk-swift/Tests/BitcoinDevKitTests/LiveElectrumClientTests.swift @@ -14,12 +14,12 @@ final class LiveElectrumClientTests: XCTestCase { ) func testSyncedBalance() throws { - let connection = try Connection.newInMemory() + let persister = try Persister.newInMemory() let wallet = try Wallet( descriptor: descriptor, changeDescriptor: changeDescriptor, network: Network.signet, - connection: connection + persister: persister ) let electrumClient: ElectrumClient = try ElectrumClient(url: SIGNET_ELECTRUM_URL) let fullScanRequest: FullScanRequest = try wallet.startFullScan().build() diff --git a/bdk-swift/Tests/BitcoinDevKitTests/LiveKyotoTests.swift b/bdk-swift/Tests/BitcoinDevKitTests/LiveKyotoTests.swift index c24118aa..8097e6aa 100644 --- a/bdk-swift/Tests/BitcoinDevKitTests/LiveKyotoTests.swift +++ b/bdk-swift/Tests/BitcoinDevKitTests/LiveKyotoTests.swift @@ -21,12 +21,12 @@ final class LiveKyotoTests: XCTestCase { } func testSuccessfullySyncs() async throws { - let connection = try Connection.newInMemory() + let persister = try Persister.newInMemory() let wallet = try Wallet( descriptor: descriptor, changeDescriptor: changeDescriptor, network: Network.signet, - connection: connection + persister: persister ) let trustedPeer = Peer(address: peer, port: nil, v2Transport: false) let lightClient = try CbfBuilder() diff --git a/bdk-swift/Tests/BitcoinDevKitTests/LiveMemoryWalletTests.swift b/bdk-swift/Tests/BitcoinDevKitTests/LiveMemoryWalletTests.swift index 62e422bc..a6e3256a 100644 --- a/bdk-swift/Tests/BitcoinDevKitTests/LiveMemoryWalletTests.swift +++ b/bdk-swift/Tests/BitcoinDevKitTests/LiveMemoryWalletTests.swift @@ -15,12 +15,12 @@ final class LiveMemoryWalletTests: XCTestCase { ) func testSyncedBalance() throws { - let connection = try Connection.newInMemory() + let persister = try Persister.newInMemory() let wallet = try Wallet( descriptor: descriptor, changeDescriptor: changeDescriptor, network: .signet, - connection: connection + persister: persister ) let esploraClient = EsploraClient(url: SIGNET_ESPLORA_URL) let fullScanRequest: FullScanRequest = try wallet.startFullScan().build() @@ -49,12 +49,12 @@ final class LiveMemoryWalletTests: XCTestCase { } func testScriptInspector() throws { - let connection = try Connection.newInMemory() + let persister = try Persister.newInMemory() let wallet = try Wallet( descriptor: descriptor, changeDescriptor: changeDescriptor, network: .signet, - connection: connection + persister: persister ) let esploraClient = EsploraClient(url: SIGNET_ESPLORA_URL) let scriptInspector = FullScriptInspector() diff --git a/bdk-swift/Tests/BitcoinDevKitTests/LiveTransactionTests.swift b/bdk-swift/Tests/BitcoinDevKitTests/LiveTransactionTests.swift index 4c1669d4..e45058d5 100644 --- a/bdk-swift/Tests/BitcoinDevKitTests/LiveTransactionTests.swift +++ b/bdk-swift/Tests/BitcoinDevKitTests/LiveTransactionTests.swift @@ -16,12 +16,12 @@ final class LiveTransactionTests: XCTestCase { ) func testSyncedBalance() throws { - let connection = try Connection.newInMemory() + let persister = try Persister.newInMemory() let wallet = try Wallet( descriptor: descriptor, changeDescriptor: changeDescriptor, network: .signet, - connection: connection + persister: persister ) let esploraClient = EsploraClient(url: SIGNET_ESPLORA_URL) let fullScanRequest: FullScanRequest = try wallet.startFullScan().build() diff --git a/bdk-swift/Tests/BitcoinDevKitTests/LiveTxBuilderTests.swift b/bdk-swift/Tests/BitcoinDevKitTests/LiveTxBuilderTests.swift index 5091b0b8..e5cc0260 100644 --- a/bdk-swift/Tests/BitcoinDevKitTests/LiveTxBuilderTests.swift +++ b/bdk-swift/Tests/BitcoinDevKitTests/LiveTxBuilderTests.swift @@ -35,12 +35,12 @@ final class LiveTxBuilderTests: XCTestCase { } func testTxBuilder() throws { - let connection = try Connection.newInMemory() + let persister = try Persister.newInMemory() let wallet = try Wallet( descriptor: descriptor, changeDescriptor: changeDescriptor, network: .signet, - connection: connection + persister: persister ) let esploraClient = EsploraClient(url: SIGNET_ESPLORA_URL) let fullScanRequest: FullScanRequest = try wallet.startFullScan().build() @@ -69,12 +69,12 @@ final class LiveTxBuilderTests: XCTestCase { } func testComplexTxBuilder() throws { - let connection = try Connection.newInMemory() + let persister = try Persister.newInMemory() let wallet = try Wallet( descriptor: descriptor, changeDescriptor: changeDescriptor, network: .signet, - connection: connection + persister: persister ) let esploraClient = EsploraClient(url: SIGNET_ESPLORA_URL) let fullScanRequest: FullScanRequest = try wallet.startFullScan().build() diff --git a/bdk-swift/Tests/BitcoinDevKitTests/LiveWalletTests.swift b/bdk-swift/Tests/BitcoinDevKitTests/LiveWalletTests.swift index 54fdbd3c..eb8784cf 100644 --- a/bdk-swift/Tests/BitcoinDevKitTests/LiveWalletTests.swift +++ b/bdk-swift/Tests/BitcoinDevKitTests/LiveWalletTests.swift @@ -36,12 +36,12 @@ final class LiveWalletTests: XCTestCase { } func testSyncedBalance() throws { - let connection = try Connection.newInMemory() + let persister = try Persister.newInMemory() let wallet = try Wallet( descriptor: descriptor, changeDescriptor: changeDescriptor, network: .signet, - connection: connection + persister: persister ) let esploraClient = EsploraClient(url: SIGNET_ESPLORA_URL) let fullScanRequest: FullScanRequest = try wallet.startFullScan().build() @@ -75,12 +75,12 @@ final class LiveWalletTests: XCTestCase { } func testBroadcastTransaction() throws { - let connection = try Connection.newInMemory() + let persister = try Persister.newInMemory() let wallet = try Wallet( descriptor: descriptor, changeDescriptor: changeDescriptor, network: .signet, - connection: connection + persister: persister ) let esploraClient = EsploraClient(url: SIGNET_ESPLORA_URL) let fullScanRequest: FullScanRequest = try wallet.startFullScan().build() diff --git a/bdk-swift/Tests/BitcoinDevKitTests/OfflinePersistenceTests.swift b/bdk-swift/Tests/BitcoinDevKitTests/OfflinePersistenceTests.swift index 35bb4935..4d88ebd4 100644 --- a/bdk-swift/Tests/BitcoinDevKitTests/OfflinePersistenceTests.swift +++ b/bdk-swift/Tests/BitcoinDevKitTests/OfflinePersistenceTests.swift @@ -26,11 +26,11 @@ final class OfflinePersistenceTests: XCTestCase { } func testPersistence() throws { - let connection = try Connection(path: dbFilePath.path) + let persister = try Persister.newSqlite(path: dbFilePath.path) let wallet = try Wallet.load( descriptor: descriptor, changeDescriptor: changeDescriptor, - connection: connection + persister: persister ) let nextAddress: AddressInfo = wallet.revealNextAddress(keychain: KeychainKind.external) print("Address: \(nextAddress)") @@ -40,7 +40,7 @@ final class OfflinePersistenceTests: XCTestCase { } func testPersistenceWithDescriptor() throws { - let connection = try Connection(path: dbFilePath.path) + let persister = try Persister.newSqlite(path: dbFilePath.path) let descriptorPub = try Descriptor( descriptor: "wpkh([9122d9e0/84'/1'/0']tpubDCYVtmaSaDzTxcgvoP5AHZNbZKZzrvoNH9KARep88vESc6MxRqAp4LmePc2eeGX6XUxBcdhAmkthWTDqygPz2wLAyHWisD299Lkdrj5egY6/0/*)#zpaanzgu", @@ -54,7 +54,7 @@ final class OfflinePersistenceTests: XCTestCase { let wallet = try Wallet.load( descriptor: descriptorPub, changeDescriptor: changeDescriptorPub, - connection: connection + persister: persister ) let nextAddress: AddressInfo = wallet.revealNextAddress(keychain: KeychainKind.external) print("Address: \(nextAddress)") diff --git a/bdk-swift/Tests/BitcoinDevKitTests/OfflineWalletTests.swift b/bdk-swift/Tests/BitcoinDevKitTests/OfflineWalletTests.swift index 7a1f4e83..ae3fc1d5 100644 --- a/bdk-swift/Tests/BitcoinDevKitTests/OfflineWalletTests.swift +++ b/bdk-swift/Tests/BitcoinDevKitTests/OfflineWalletTests.swift @@ -12,12 +12,12 @@ final class OfflineWalletTests: XCTestCase { ) func testNewAddress() throws { - let connection = try Connection.newInMemory() + let persister = try Persister.newInMemory() let wallet = try Wallet( descriptor: descriptor, changeDescriptor: changeDescriptor, network: .signet, - connection: connection + persister: persister ) let addressInfo: AddressInfo = wallet.revealNextAddress(keychain: KeychainKind.external) @@ -34,12 +34,12 @@ final class OfflineWalletTests: XCTestCase { } func testBalance() throws { - let connection = try Connection.newInMemory() + let persister = try Persister.newInMemory() let wallet = try Wallet( descriptor: descriptor, changeDescriptor: changeDescriptor, network: .signet, - connection: connection + persister: persister ) XCTAssertEqual(wallet.balance().total.toSat(), 0)