diff --git a/Cargo.toml b/Cargo.toml index e625d581f..b190ba88f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ members = [ "example-crates/wallet_electrum", "example-crates/wallet_esplora_blocking", "example-crates/wallet_esplora_async", + "example-crates/wallet_rpc", "nursery/tmp_plan", "nursery/coin_select" ] diff --git a/example-crates/example_bitcoind_rpc_polling/README.md b/example-crates/example_bitcoind_rpc_polling/README.md new file mode 100644 index 000000000..fef82ab1c --- /dev/null +++ b/example-crates/example_bitcoind_rpc_polling/README.md @@ -0,0 +1,68 @@ +# Example RPC CLI + +### Simple Regtest Test + +1. Start local regtest bitcoind. + ``` + mkdir -p /tmp/regtest/bitcoind + bitcoind -regtest -server -fallbackfee=0.0002 -rpcuser= -rpcpassword= -datadir=/tmp/regtest/bitcoind -daemon + ``` +2. Create a test bitcoind wallet and set bitcoind env. + ``` + bitcoin-cli -datadir=/tmp/regtest/bitcoind -regtest -rpcuser= -rpcpassword= -named createwallet wallet_name="test" + export RPC_URL=127.0.0.1:18443 + export RPC_USER= + export RPC_PASS= + ``` +3. Get test bitcoind wallet info. + ``` + bitcoin-cli -rpcwallet="test" -rpcuser= -rpcpassword= -datadir=/tmp/regtest/bitcoind -regtest getwalletinfo + ``` +4. Get new test bitcoind wallet address. + ``` + BITCOIND_ADDRESS=$(bitcoin-cli -rpcwallet="test" -datadir=/tmp/regtest/bitcoind -regtest -rpcuser= -rpcpassword= getnewaddress) + echo $BITCOIND_ADDRESS + ``` +5. Generate 101 blocks with reward to test bitcoind wallet address. + ``` + bitcoin-cli -datadir=/tmp/regtest/bitcoind -regtest -rpcuser= -rpcpassword= generatetoaddress 101 $BITCOIND_ADDRESS + ``` +6. Verify test bitcoind wallet balance. + ``` + bitcoin-cli -rpcwallet="test" -datadir=/tmp/regtest/bitcoind -regtest -rpcuser= -rpcpassword= getbalances + ``` +7. Set descriptor env and get address from RPC CLI wallet. + ``` + export DESCRIPTOR="wpkh(tprv8ZgxMBicQKsPfK9BTf82oQkHhawtZv19CorqQKPFeaHDMA4dXYX6eWsJGNJ7VTQXWmoHdrfjCYuDijcRmNFwSKcVhswzqs4fugE8turndGc/1/*)" + cargo run -- --network regtest address next + ``` +8. Send 5 test bitcoin to RPC CLI wallet. + ``` + bitcoin-cli -rpcwallet="test" -datadir=/tmp/regtest/bitcoind -regtest -rpcuser= -rpcpassword= sendtoaddress
5 + ``` +9. Sync blockchain with RPC CLI wallet. + ``` + cargo run -- --network regtest sync + + ``` +10. Get RPC CLI wallet unconfirmed balances. + ``` + cargo run -- --network regtest balance + ``` +11. Generate 1 block with reward to test bitcoind wallet address. + ``` + bitcoin-cli -datadir=/tmp/regtest/bitcoind -rpcuser= -rpcpassword= -regtest generatetoaddress 10 $BITCOIND_ADDRESS + ``` +12. Sync the blockchain with RPC CLI wallet. + ``` + cargo run -- --network regtest sync + + ``` +13. Get RPC CLI wallet confirmed balances. + ``` + cargo run -- --network regtest balance + ``` +14. Get RPC CLI wallet transactions. + ``` + cargo run -- --network regtest txout list + ``` \ No newline at end of file diff --git a/example-crates/example_bitcoind_rpc_polling/src/main.rs b/example-crates/example_bitcoind_rpc_polling/src/main.rs index 648962c28..aff5fc99e 100644 --- a/example-crates/example_bitcoind_rpc_polling/src/main.rs +++ b/example-crates/example_bitcoind_rpc_polling/src/main.rs @@ -191,7 +191,7 @@ fn main() -> anyhow::Result<()> { introduce_older_blocks: false, }) .expect("must always apply as we receive blocks in order from emitter"); - let graph_changeset = graph.apply_block_relevant(emission.block, height); + let graph_changeset = graph.apply_block_relevant(&emission.block, height); db.stage((chain_changeset, graph_changeset)); // commit staged db changes in intervals @@ -307,7 +307,7 @@ fn main() -> anyhow::Result<()> { .apply_update(chain_update) .expect("must always apply as we receive blocks in order from emitter"); let graph_changeset = - graph.apply_block_relevant(block_emission.block, height); + graph.apply_block_relevant(&block_emission.block, height); (chain_changeset, graph_changeset) } Emission::Mempool(mempool_txs) => { diff --git a/example-crates/wallet_rpc/Cargo.toml b/example-crates/wallet_rpc/Cargo.toml new file mode 100644 index 000000000..174144e9b --- /dev/null +++ b/example-crates/wallet_rpc/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "wallet_rpc" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bdk = { path = "../../crates/bdk" } +bdk_file_store = { path = "../../crates/file_store" } +bdk_bitcoind_rpc = { path = "../../crates/bitcoind_rpc" } + +anyhow = "1" +clap = { version = "3.2.25", features = ["derive", "env"] } +ctrlc = "2.0.1" diff --git a/example-crates/wallet_rpc/README.md b/example-crates/wallet_rpc/README.md new file mode 100644 index 000000000..0a2cc2946 --- /dev/null +++ b/example-crates/wallet_rpc/README.md @@ -0,0 +1,45 @@ +# Wallet RPC Example + +``` +$ cargo run --bin wallet_rpc -- --help + +wallet_rpc 0.1.0 +Bitcoind RPC example usign `bdk::Wallet` + +USAGE: + wallet_rpc [OPTIONS] [CHANGE_DESCRIPTOR] + +ARGS: + Wallet descriptor [env: DESCRIPTOR=] + Wallet change descriptor [env: CHANGE_DESCRIPTOR=] + +OPTIONS: + --db-path + Where to store wallet data [env: BDK_DB_PATH=] [default: .bdk_wallet_rpc_example.db] + + -h, --help + Print help information + + --network + Bitcoin network to connect to [env: BITCOIN_NETWORK=] [default: testnet] + + --rpc-cookie + RPC auth cookie file [env: RPC_COOKIE=] + + --rpc-pass + RPC auth password [env: RPC_PASS=] + + --rpc-user + RPC auth username [env: RPC_USER=] + + --start-height + Earliest block height to start sync from [env: START_HEIGHT=] [default: 481824] + + --url + RPC URL [env: RPC_URL=] [default: 127.0.0.1:8332] + + -V, --version + Print version information + +``` + diff --git a/example-crates/wallet_rpc/src/main.rs b/example-crates/wallet_rpc/src/main.rs new file mode 100644 index 000000000..dc3b8bcdc --- /dev/null +++ b/example-crates/wallet_rpc/src/main.rs @@ -0,0 +1,182 @@ +use bdk::{ + bitcoin::{Block, Network, Transaction}, + wallet::Wallet, +}; +use bdk_bitcoind_rpc::{ + bitcoincore_rpc::{Auth, Client, RpcApi}, + Emitter, +}; +use bdk_file_store::Store; +use clap::{self, Parser}; +use std::{path::PathBuf, sync::mpsc::sync_channel, thread::spawn, time::Instant}; + +const DB_MAGIC: &str = "bdk-rpc-wallet-example"; + +/// Bitcoind RPC example usign `bdk::Wallet`. +/// +/// This syncs the chain block-by-block and prints the current balance, transaction count and UTXO +/// count. +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +#[clap(propagate_version = true)] +pub struct Args { + /// Wallet descriptor + #[clap(env = "DESCRIPTOR")] + pub descriptor: String, + /// Wallet change descriptor + #[clap(env = "CHANGE_DESCRIPTOR")] + pub change_descriptor: Option, + /// Earliest block height to start sync from + #[clap(env = "START_HEIGHT", long, default_value = "481824")] + pub start_height: u32, + /// Bitcoin network to connect to + #[clap(env = "BITCOIN_NETWORK", long, default_value = "testnet")] + pub network: Network, + /// Where to store wallet data + #[clap( + env = "BDK_DB_PATH", + long, + default_value = ".bdk_wallet_rpc_example.db" + )] + pub db_path: PathBuf, + + /// RPC URL + #[clap(env = "RPC_URL", long, default_value = "127.0.0.1:8332")] + pub url: String, + /// RPC auth cookie file + #[clap(env = "RPC_COOKIE", long)] + pub rpc_cookie: Option, + /// RPC auth username + #[clap(env = "RPC_USER", long)] + pub rpc_user: Option, + /// RPC auth password + #[clap(env = "RPC_PASS", long)] + pub rpc_pass: Option, +} + +impl Args { + fn client(&self) -> anyhow::Result { + Ok(Client::new( + &self.url, + match (&self.rpc_cookie, &self.rpc_user, &self.rpc_pass) { + (None, None, None) => Auth::None, + (Some(path), _, _) => Auth::CookieFile(path.clone()), + (_, Some(user), Some(pass)) => Auth::UserPass(user.clone(), pass.clone()), + (_, Some(_), None) => panic!("rpc auth: missing rpc_pass"), + (_, None, Some(_)) => panic!("rpc auth: missing rpc_user"), + }, + )?) + } +} + +#[derive(Debug)] +enum Emission { + SigTerm, + Block(bdk_bitcoind_rpc::BlockEvent), + Mempool(Vec<(Transaction, u64)>), +} + +fn main() -> anyhow::Result<()> { + let args = Args::parse(); + + let rpc_client = args.client()?; + println!( + "Connected to Bitcoin Core RPC at {:?}", + rpc_client.get_blockchain_info().unwrap() + ); + + let start_load_wallet = Instant::now(); + let mut wallet = Wallet::new_or_load( + &args.descriptor, + args.change_descriptor.as_ref(), + Store::::open_or_create_new(DB_MAGIC.as_bytes(), args.db_path)?, + args.network, + )?; + println!( + "Loaded wallet in {}s", + start_load_wallet.elapsed().as_secs_f32() + ); + + let balance = wallet.get_balance(); + println!("Wallet balance before syncing: {} sats", balance.total()); + + let wallet_tip = wallet.latest_checkpoint(); + println!( + "Wallet tip: {} at height {}", + wallet_tip.hash(), + wallet_tip.height() + ); + + let (sender, receiver) = sync_channel::(21); + + let signal_sender = sender.clone(); + ctrlc::set_handler(move || { + signal_sender + .send(Emission::SigTerm) + .expect("failed to send sigterm") + }); + + let emitter_tip = wallet_tip.clone(); + spawn(move || -> Result<(), anyhow::Error> { + let mut emitter = Emitter::new(&rpc_client, emitter_tip, args.start_height); + while let Some(emission) = emitter.next_block()? { + sender.send(Emission::Block(emission))?; + } + sender.send(Emission::Mempool(emitter.mempool()?))?; + Ok(()) + }); + + let mut blocks_received = 0_usize; + for emission in receiver { + match emission { + Emission::SigTerm => { + println!("Sigterm received, exiting..."); + break; + } + Emission::Block(block_emission) => { + blocks_received += 1; + let height = block_emission.block_height(); + let hash = block_emission.block_hash(); + let connected_to = block_emission.connected_to(); + let start_apply_block = Instant::now(); + wallet.apply_block_connected_to(&block_emission.block, height, connected_to)?; + wallet.commit()?; + let elapsed = start_apply_block.elapsed().as_secs_f32(); + println!( + "Applied block {} at height {} in {}s", + hash, height, elapsed + ); + } + Emission::Mempool(mempool_emission) => { + let start_apply_mempool = Instant::now(); + wallet.apply_unconfirmed_txs(mempool_emission.iter().map(|(tx, time)| (tx, *time))); + wallet.commit()?; + println!( + "Applied unconfirmed transactions in {}s", + start_apply_mempool.elapsed().as_secs_f32() + ); + break; + } + } + } + let wallet_tip_end = wallet.latest_checkpoint(); + let balance = wallet.get_balance(); + println!( + "Synced {} blocks in {}s", + blocks_received, + start_load_wallet.elapsed().as_secs_f32(), + ); + println!( + "Wallet tip is '{}:{}'", + wallet_tip_end.height(), + wallet_tip_end.hash() + ); + println!("Wallet balance is {} sats", balance.total()); + println!( + "Wallet has {} transactions and {} utxos", + wallet.transactions().count(), + wallet.list_unspent().count() + ); + + Ok(()) +}