From dca89411451b33ee0496b1c54438e53997cd9a26 Mon Sep 17 00:00:00 2001 From: valued mammal Date: Tue, 15 Oct 2024 14:48:33 -0400 Subject: [PATCH] docs: show example migration from bdk to bdk_wallet --- Cargo.toml | 1 + .../example_migrate_wallet/Cargo.toml | 12 ++ .../example_migrate_wallet/src/main.rs | 131 ++++++++++++++++++ 3 files changed, 144 insertions(+) create mode 100644 example-crates/example_migrate_wallet/Cargo.toml create mode 100644 example-crates/example_migrate_wallet/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 2abc16bd8..aa10ee28d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ members = [ "example-crates/example_wallet_esplora_blocking", "example-crates/example_wallet_esplora_async", "example-crates/example_wallet_rpc", + "example-crates/example_migrate_wallet", ] [workspace.package] diff --git a/example-crates/example_migrate_wallet/Cargo.toml b/example-crates/example_migrate_wallet/Cargo.toml new file mode 100644 index 000000000..f45da7115 --- /dev/null +++ b/example-crates/example_migrate_wallet/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "example_migrate_wallet" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1" +# TODO: Release bdk 0.30 to enable sqlite feature +#bdk = { version = "0.30", features = ["sqlite"] } +bdk = { git = "https://github.com/ValuedMammal/bdk", branch = "release/0.29-deps-rusqlite", features = ["sqlite"] } +bdk_wallet = { path = "../../crates/wallet", features = ["rusqlite"] } +bdk_electrum = { path = "../../crates/electrum" } diff --git a/example-crates/example_migrate_wallet/src/main.rs b/example-crates/example_migrate_wallet/src/main.rs new file mode 100644 index 000000000..3a936732a --- /dev/null +++ b/example-crates/example_migrate_wallet/src/main.rs @@ -0,0 +1,131 @@ +use std::collections::{BTreeSet, HashSet}; +use std::io::Write; + +use bdk::bitcoin::Network; +use bdk::database::SqliteDatabase; +use bdk::wallet::{AddressIndex, Wallet}; + +use bdk_electrum::{electrum_client, BdkElectrumClient}; +use bdk_wallet::rusqlite; + +const EXTERNAL: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; +const INTERNAL: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)"; +const ELECTRUM_URL: &str = "ssl://electrum.blockstream.info:60002"; +const NETWORK: Network = Network::Testnet; + +const BDK_DB_PATH: &str = ".bdk-example.sqlite"; +const BDK_WALLET_DB_PATH: &str = ".bdk-wallet-example.sqlite"; + +// Steps for migrating wallet parameters from the old `bdk` 0.30 to the new `bdk_wallet` 1.0. +// These steps can be applied to wallets backed by a SQLite database. For example to read an +// existing database, change `BDK_DB_PATH` above to point to the location of the old database +// file. You may also want to hard code the remaining parameters (descriptors, network, etc) +// to fit your setup. Note: because we're migrating to a new database there must not already +// exist a persisted wallet at the new path `BDK_WALLET_DB_PATH`. + +// Usage: `cargo run --bin example_migrate_wallet` + +fn main() -> anyhow::Result<()> { + // Open old wallet + let db = SqliteDatabase::new(BDK_DB_PATH); + let old_wallet = Wallet::new(EXTERNAL, Some(INTERNAL), NETWORK, db)?; + + // Get last revealed addresses for each keychain + let addr = old_wallet.get_address(AddressIndex::LastUnused)?; + println!("Last revealed external {} {}", addr.index, addr.address); + let external_derivation_index = addr.index; + let last_revealed_external = addr.address.to_string(); + + let addr = old_wallet.get_internal_address(AddressIndex::LastUnused)?; + println!("Last revealed internal {} {}", addr.index, addr.address); + let internal_derivation_index = addr.index; + let last_revealed_internal = addr.address.to_string(); + + // Get unspent balance + let old_unspent = old_wallet.list_unspent()?; + let old_balance = old_wallet.get_balance()?; + println!("Balance before sync: {} sat", old_balance.get_total()); + + // Create new wallet + // For the new bdk wallet we pass in the same descriptors as before. If the given descriptors + // contain secret keys the wallet will be able to sign transactions as well. + let mut db = rusqlite::Connection::open(BDK_WALLET_DB_PATH)?; + let mut new_wallet = match bdk_wallet::Wallet::create(EXTERNAL, INTERNAL) + .network(bdk_wallet::bitcoin::Network::Signet) + .create_wallet(&mut db) + { + Ok(wallet) => wallet, + Err(_) => anyhow::bail!("should not have existing db"), + }; + + // Retore revealed addresses + let _ = new_wallet.reveal_addresses_to( + bdk_wallet::KeychainKind::External, + external_derivation_index, + ); + let _ = new_wallet.reveal_addresses_to( + bdk_wallet::KeychainKind::Internal, + internal_derivation_index, + ); + + // Remember to persist the new wallet + new_wallet.persist(&mut db)?; + + println!("\n========== New database created. =========="); + + let addr = new_wallet + .list_unused_addresses(bdk_wallet::KeychainKind::External) + .last() + .unwrap(); + assert_eq!(addr.to_string(), last_revealed_external); + println!("Last revealed external {} {}", addr.index, addr.address); + let addr = new_wallet + .list_unused_addresses(bdk_wallet::KeychainKind::Internal) + .last() + .unwrap(); + println!("Last revealed internal {} {}", addr.index, addr.address); + assert_eq!(addr.to_string(), last_revealed_internal); + + // Now that we migrated the wallet details, you likely want to rescan the blockchain + // to restore the wallet's transaction data. Here we're using the bdk_electrum client + // with a gap limit of 10. We also pass the `true` parameter to `full_scan` to indicate + // we want to collect additional transaction data (previous txouts) that are important + // for a fully functioning wallet. + + let client = BdkElectrumClient::new(electrum_client::Client::new(ELECTRUM_URL)?); + let request = new_wallet.start_full_scan().inspect({ + let mut stdout = std::io::stdout(); + let mut once = HashSet::new(); + move |k, spk_i, _| { + if once.insert(k) { + print!("\nScanning keychain [{:?}]", k); + } + print!(" {:<3}", spk_i); + stdout.flush().unwrap(); + } + }); + + let update = client.full_scan(request, 10, 5, true)?; + + new_wallet.apply_update(update)?; + new_wallet.persist(&mut db)?; + + // mapping unspent outpoints to string + let new_unspent: BTreeSet<_> = new_wallet + .list_unspent() + .map(|output| output.outpoint.to_string()) + .collect(); + assert_eq!( + new_unspent, + old_unspent + .into_iter() + .map(|output| output.outpoint.to_string()) + .collect::>() + ); + + let new_balance = new_wallet.balance(); + assert_eq!(new_balance.total().to_sat(), old_balance.get_total()); + println!("\nBalance after sync: {} sat", new_balance.total().to_sat()); + + Ok(()) +}