Skip to content

Commit 28375ab

Browse files
committed
feat: add example with rpc and redb
Also added manifest and binary files that had forgotten to add to git.
1 parent d6a96fa commit 28375ab

5 files changed

Lines changed: 361 additions & 0 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "example_wallet_esplora_blocking_redb"
3+
version = "0.1.0"
4+
edition = "2024"
5+
authors.workspace = true
6+
7+
[dependencies]
8+
bdk_wallet = { version = "2.0.0" }
9+
bdk_esplora = { version = "0.22.0", features = ["blocking"] }
10+
anyhow = "1"
11+
bdk_redb = { git = "https://github.com/110CodingP/bdk_redb" }
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
use std::{collections::BTreeSet, io::Write};
2+
3+
use bdk_esplora::{esplora_client, EsploraExt};
4+
use bdk_wallet::{
5+
bitcoin::{Amount, Network},
6+
KeychainKind, SignOptions, Wallet,
7+
};
8+
9+
use bdk_redb::Store;
10+
const DB_PATH: &str = "bdk-example-esplora-blocking.db";
11+
const SEND_AMOUNT: Amount = Amount::from_sat(5000);
12+
const STOP_GAP: usize = 5;
13+
const PARALLEL_REQUESTS: usize = 5;
14+
15+
const NETWORK: Network = Network::Signet;
16+
const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
17+
const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
18+
const ESPLORA_URL: &str = "http://signet.bitcoindevkit.net";
19+
20+
fn main() -> Result<(), anyhow::Error> {
21+
let mut store = Store::new(DB_PATH, "wallet1".to_string())?;
22+
23+
let wallet_opt = Wallet::load()
24+
.descriptor(KeychainKind::External, Some(EXTERNAL_DESC))
25+
.descriptor(KeychainKind::Internal, Some(INTERNAL_DESC))
26+
.extract_keys()
27+
.check_network(NETWORK)
28+
.load_wallet(&mut store)?;
29+
let mut wallet = match wallet_opt {
30+
Some(wallet) => wallet,
31+
None => Wallet::create(EXTERNAL_DESC, INTERNAL_DESC)
32+
.network(NETWORK)
33+
.create_wallet(&mut store)?,
34+
};
35+
36+
let address = wallet.next_unused_address(KeychainKind::External);
37+
wallet.persist(&mut store)?;
38+
println!(
39+
"Next unused address: ({}) {}",
40+
address.index, address.address
41+
);
42+
43+
let balance = wallet.balance();
44+
println!("Wallet balance before syncing: {}", balance.total());
45+
46+
print!("Syncing...");
47+
let client = esplora_client::Builder::new(ESPLORA_URL).build_blocking();
48+
49+
let request = wallet.start_full_scan().inspect({
50+
let mut stdout = std::io::stdout();
51+
let mut once = BTreeSet::<KeychainKind>::new();
52+
move |keychain, spk_i, _| {
53+
if once.insert(keychain) {
54+
print!("\nScanning keychain [{:?}] ", keychain);
55+
}
56+
print!(" {:<3}", spk_i);
57+
stdout.flush().expect("must flush")
58+
}
59+
});
60+
61+
let update = client.full_scan(request, STOP_GAP, PARALLEL_REQUESTS)?;
62+
63+
wallet.apply_update(update)?;
64+
wallet.persist(&mut store)?;
65+
println!();
66+
67+
let balance = wallet.balance();
68+
println!("Wallet balance after syncing: {}", balance.total());
69+
70+
if balance.total() < SEND_AMOUNT {
71+
println!(
72+
"Please send at least {} to the receiving address",
73+
SEND_AMOUNT
74+
);
75+
std::process::exit(0);
76+
}
77+
78+
let mut tx_builder = wallet.build_tx();
79+
tx_builder.add_recipient(address.script_pubkey(), SEND_AMOUNT);
80+
81+
let mut psbt = tx_builder.finish()?;
82+
let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
83+
assert!(finalized);
84+
85+
let tx = psbt.extract_tx()?;
86+
client.broadcast(&tx)?;
87+
println!("Tx broadcasted! Txid: {}", tx.compute_txid());
88+
89+
Ok(())
90+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "example_wallet_rpc_redb"
3+
version = "0.1.0"
4+
edition = "2024"
5+
authors.workspace = true
6+
7+
[dependencies]
8+
bdk_wallet = { version = "2.0.0" }
9+
bdk_bitcoind_rpc = { version = "0.20.0" }
10+
11+
anyhow = "1"
12+
clap = { version = "4.5.17", features = ["derive", "env"] }
13+
ctrlc = "3.4.6"
14+
bdk_redb = { git = "https://github.com/110CodingP/bdk_redb" }
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Wallet RPC Example
2+
3+
```
4+
$ cargo run --bin example_wallet_rpc -- --help
5+
6+
Bitcoind RPC example using `bdk_wallet::Wallet`
7+
8+
Usage: example_wallet_rpc [OPTIONS] <DESCRIPTOR> [CHANGE_DESCRIPTOR]
9+
10+
Arguments:
11+
<DESCRIPTOR> Wallet descriptor [env: DESCRIPTOR=]
12+
[CHANGE_DESCRIPTOR] Wallet change descriptor [env: CHANGE_DESCRIPTOR=]
13+
14+
Options:
15+
--start-height <START_HEIGHT> Earliest block height to start sync from [env: START_HEIGHT=] [default: 0]
16+
17+
--network <NETWORK> Bitcoin network to connect to [env: BITCOIN_NETWORK=] [default: regtest]
18+
19+
--db-path <DB_PATH> Where to store wallet data [env: BDK_DB_PATH=] [default: .bdk_wallet_rpc_example.db]
20+
21+
--url <URL> RPC URL [env: RPC_URL=] [default: 127.0.0.1:18443]
22+
23+
--rpc-cookie <RPC_COOKIE> RPC auth cookie file [env: RPC_COOKIE=]
24+
25+
--rpc-user <RPC_USER> RPC auth username [env: RPC_USER=]
26+
27+
--rpc-pass <RPC_PASS> RPC auth password [env: RPC_PASS=]
28+
29+
--wallet-name <WALLET_NAME> Name of wallet [env: WALLET_NAME=] [default: wallet1]
30+
31+
-h, --help Print help
32+
33+
-V, --version Print version
34+
35+
```
36+
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
use bdk_bitcoind_rpc::{
2+
bitcoincore_rpc::{Auth, Client, RpcApi},
3+
Emitter, MempoolEvent,
4+
};
5+
6+
use bdk_redb::Store;
7+
use bdk_wallet::{
8+
bitcoin::{Block, Network},
9+
KeychainKind, Wallet,
10+
};
11+
use clap::{self, Parser};
12+
use std::{
13+
path::PathBuf,
14+
sync::{mpsc::sync_channel, Arc},
15+
thread::spawn,
16+
time::Instant,
17+
};
18+
19+
20+
/// Bitcoind RPC example using `bdk_wallet::Wallet`.
21+
///
22+
/// This syncs the chain block-by-block and prints the current balance, transaction count and UTXO
23+
/// count.
24+
#[derive(Parser, Debug)]
25+
#[clap(author, version, about, long_about = None)]
26+
#[clap(propagate_version = true)]
27+
pub struct Args {
28+
/// Wallet descriptor
29+
#[clap(env = "DESCRIPTOR")]
30+
pub descriptor: String,
31+
/// Wallet change descriptor
32+
#[clap(env = "CHANGE_DESCRIPTOR")]
33+
pub change_descriptor: Option<String>,
34+
/// Earliest block height to start sync from
35+
#[clap(env = "START_HEIGHT", long, default_value = "0")]
36+
pub start_height: u32,
37+
/// Bitcoin network to connect to
38+
#[clap(env = "BITCOIN_NETWORK", long, default_value = "regtest")]
39+
pub network: Network,
40+
/// Where to store wallet data
41+
#[clap(
42+
env = "BDK_DB_PATH",
43+
long,
44+
default_value = ".bdk_wallet_rpc_example.db"
45+
)]
46+
pub db_path: PathBuf,
47+
48+
/// RPC URL
49+
#[clap(env = "RPC_URL", long, default_value = "127.0.0.1:18443")]
50+
pub url: String,
51+
/// RPC auth cookie file
52+
#[clap(env = "RPC_COOKIE", long)]
53+
pub rpc_cookie: Option<PathBuf>,
54+
/// RPC auth username
55+
#[clap(env = "RPC_USER", long)]
56+
pub rpc_user: Option<String>,
57+
/// RPC auth password
58+
#[clap(env = "RPC_PASS", long)]
59+
pub rpc_pass: Option<String>,
60+
/// Wallet name
61+
#[clap(env = "WALLET_NAME", long, default_value = "wallet1")]
62+
pub wallet_name: String,
63+
}
64+
65+
impl Args {
66+
fn client(&self) -> anyhow::Result<Client> {
67+
Ok(Client::new(
68+
&self.url,
69+
match (&self.rpc_cookie, &self.rpc_user, &self.rpc_pass) {
70+
(None, None, None) => Auth::None,
71+
(Some(path), _, _) => Auth::CookieFile(path.clone()),
72+
(_, Some(user), Some(pass)) => Auth::UserPass(user.clone(), pass.clone()),
73+
(_, Some(_), None) => panic!("rpc auth: missing rpc_pass"),
74+
(_, None, Some(_)) => panic!("rpc auth: missing rpc_user"),
75+
},
76+
)?)
77+
}
78+
}
79+
80+
#[derive(Debug)]
81+
enum Emission {
82+
SigTerm,
83+
Block(bdk_bitcoind_rpc::BlockEvent<Block>),
84+
Mempool(MempoolEvent),
85+
}
86+
87+
fn main() -> anyhow::Result<()> {
88+
let args = Args::parse();
89+
90+
let rpc_client = Arc::new(args.client()?);
91+
println!(
92+
"Connected to Bitcoin Core RPC at {:?}",
93+
rpc_client.get_blockchain_info().unwrap()
94+
);
95+
96+
let start_load_wallet = Instant::now();
97+
let mut store =
98+
Store::new(args.db_path, args.wallet_name)?;
99+
let wallet_opt = Wallet::load()
100+
.descriptor(KeychainKind::External, Some(args.descriptor.clone()))
101+
.descriptor(KeychainKind::Internal, args.change_descriptor.clone())
102+
.extract_keys()
103+
.check_network(args.network)
104+
.load_wallet(&mut store)?;
105+
let mut wallet = match wallet_opt {
106+
Some(wallet) => wallet,
107+
None => match &args.change_descriptor {
108+
Some(change_desc) => Wallet::create(args.descriptor.clone(), change_desc.clone())
109+
.network(args.network)
110+
.create_wallet(&mut store)?,
111+
None => Wallet::create_single(args.descriptor.clone())
112+
.network(args.network)
113+
.create_wallet(&mut store)?,
114+
},
115+
};
116+
println!(
117+
"Loaded wallet in {}s",
118+
start_load_wallet.elapsed().as_secs_f32()
119+
);
120+
121+
let balance = wallet.balance();
122+
println!("Wallet balance before syncing: {}", balance.total());
123+
124+
let wallet_tip = wallet.latest_checkpoint();
125+
println!(
126+
"Wallet tip: {} at height {}",
127+
wallet_tip.hash(),
128+
wallet_tip.height()
129+
);
130+
131+
let (sender, receiver) = sync_channel::<Emission>(21);
132+
133+
let signal_sender = sender.clone();
134+
let _ = ctrlc::set_handler(move || {
135+
signal_sender
136+
.send(Emission::SigTerm)
137+
.expect("failed to send sigterm")
138+
});
139+
140+
let mut emitter = Emitter::new(
141+
rpc_client,
142+
wallet_tip,
143+
args.start_height,
144+
wallet
145+
.transactions()
146+
.filter(|tx| tx.chain_position.is_unconfirmed()),
147+
);
148+
spawn(move || -> Result<(), anyhow::Error> {
149+
while let Some(emission) = emitter.next_block()? {
150+
sender.send(Emission::Block(emission))?;
151+
}
152+
sender.send(Emission::Mempool(emitter.mempool()?))?;
153+
Ok(())
154+
});
155+
156+
let mut blocks_received = 0_usize;
157+
for emission in receiver {
158+
match emission {
159+
Emission::SigTerm => {
160+
println!("Sigterm received, exiting...");
161+
break;
162+
}
163+
Emission::Block(block_emission) => {
164+
blocks_received += 1;
165+
let height = block_emission.block_height();
166+
let hash = block_emission.block_hash();
167+
let connected_to = block_emission.connected_to();
168+
let start_apply_block = Instant::now();
169+
wallet.apply_block_connected_to(&block_emission.block, height, connected_to)?;
170+
wallet.persist(&mut store)?;
171+
let elapsed = start_apply_block.elapsed().as_secs_f32();
172+
println!(
173+
"Applied block {} at height {} in {}s",
174+
hash, height, elapsed
175+
);
176+
}
177+
Emission::Mempool(event) => {
178+
let start_apply_mempool = Instant::now();
179+
wallet.apply_evicted_txs(event.evicted_ats());
180+
wallet.apply_unconfirmed_txs(event.new_txs);
181+
wallet.persist(&mut store)?;
182+
println!(
183+
"Applied unconfirmed transactions in {}s",
184+
start_apply_mempool.elapsed().as_secs_f32()
185+
);
186+
break;
187+
}
188+
}
189+
}
190+
let wallet_tip_end = wallet.latest_checkpoint();
191+
let balance = wallet.balance();
192+
println!(
193+
"Synced {} blocks in {}s",
194+
blocks_received,
195+
start_load_wallet.elapsed().as_secs_f32(),
196+
);
197+
println!(
198+
"Wallet tip is '{}:{}'",
199+
wallet_tip_end.height(),
200+
wallet_tip_end.hash()
201+
);
202+
println!("Wallet balance is {}", balance.total());
203+
println!(
204+
"Wallet has {} transactions and {} utxos",
205+
wallet.transactions().count(),
206+
wallet.list_unspent().count()
207+
);
208+
209+
Ok(())
210+
}

0 commit comments

Comments
 (0)