Skip to content

Commit f9d07fa

Browse files
feat(example): add RPC wallet example
Co-authored-by: Vladimir Fomene <vladimirfomene@gmail.com> Co-authored-by: 志宇 <hello@evanlinjin.me>
1 parent 79e0208 commit f9d07fa

File tree

6 files changed

+313
-2
lines changed

6 files changed

+313
-2
lines changed

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ members = [
1515
"example-crates/wallet_electrum",
1616
"example-crates/wallet_esplora_blocking",
1717
"example-crates/wallet_esplora_async",
18+
"example-crates/wallet_rpc",
1819
"nursery/tmp_plan",
1920
"nursery/coin_select"
2021
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Example RPC CLI
2+
3+
### Simple Regtest Test
4+
5+
1. Start local regtest bitcoind.
6+
```
7+
mkdir -p /tmp/regtest/bitcoind
8+
bitcoind -regtest -server -fallbackfee=0.0002 -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> -datadir=/tmp/regtest/bitcoind -daemon
9+
```
10+
2. Create a test bitcoind wallet and set bitcoind env.
11+
```
12+
bitcoin-cli -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> -named createwallet wallet_name="test"
13+
export RPC_URL=127.0.0.1:18443
14+
export RPC_USER=<your-rpc-username>
15+
export RPC_PASS=<your-rpc-password>
16+
```
17+
3. Get test bitcoind wallet info.
18+
```
19+
bitcoin-cli -rpcwallet="test" -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> -datadir=/tmp/regtest/bitcoind -regtest getwalletinfo
20+
```
21+
4. Get new test bitcoind wallet address.
22+
```
23+
BITCOIND_ADDRESS=$(bitcoin-cli -rpcwallet="test" -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> getnewaddress)
24+
echo $BITCOIND_ADDRESS
25+
```
26+
5. Generate 101 blocks with reward to test bitcoind wallet address.
27+
```
28+
bitcoin-cli -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> generatetoaddress 101 $BITCOIND_ADDRESS
29+
```
30+
6. Verify test bitcoind wallet balance.
31+
```
32+
bitcoin-cli -rpcwallet="test" -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> getbalances
33+
```
34+
7. Set descriptor env and get address from RPC CLI wallet.
35+
```
36+
export DESCRIPTOR="wpkh(tprv8ZgxMBicQKsPfK9BTf82oQkHhawtZv19CorqQKPFeaHDMA4dXYX6eWsJGNJ7VTQXWmoHdrfjCYuDijcRmNFwSKcVhswzqs4fugE8turndGc/1/*)"
37+
cargo run -- --network regtest address next
38+
```
39+
8. Send 5 test bitcoin to RPC CLI wallet.
40+
```
41+
bitcoin-cli -rpcwallet="test" -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> sendtoaddress <address> 5
42+
```
43+
9. Sync blockchain with RPC CLI wallet.
44+
```
45+
cargo run -- --network regtest sync
46+
<CNTRL-C to stop syncing>
47+
```
48+
10. Get RPC CLI wallet unconfirmed balances.
49+
```
50+
cargo run -- --network regtest balance
51+
```
52+
11. Generate 1 block with reward to test bitcoind wallet address.
53+
```
54+
bitcoin-cli -datadir=/tmp/regtest/bitcoind -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> -regtest generatetoaddress 10 $BITCOIND_ADDRESS
55+
```
56+
12. Sync the blockchain with RPC CLI wallet.
57+
```
58+
cargo run -- --network regtest sync
59+
<CNTRL-C to stop syncing>
60+
```
61+
13. Get RPC CLI wallet confirmed balances.
62+
```
63+
cargo run -- --network regtest balance
64+
```
65+
14. Get RPC CLI wallet transactions.
66+
```
67+
cargo run -- --network regtest txout list
68+
```

example-crates/example_bitcoind_rpc_polling/src/main.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ fn main() -> anyhow::Result<()> {
191191
introduce_older_blocks: false,
192192
})
193193
.expect("must always apply as we receive blocks in order from emitter");
194-
let graph_changeset = graph.apply_block_relevant(emission.block, height);
194+
let graph_changeset = graph.apply_block_relevant(&emission.block, height);
195195
db.stage((chain_changeset, graph_changeset));
196196

197197
// commit staged db changes in intervals
@@ -307,7 +307,7 @@ fn main() -> anyhow::Result<()> {
307307
.apply_update(chain_update)
308308
.expect("must always apply as we receive blocks in order from emitter");
309309
let graph_changeset =
310-
graph.apply_block_relevant(block_emission.block, height);
310+
graph.apply_block_relevant(&block_emission.block, height);
311311
(chain_changeset, graph_changeset)
312312
}
313313
Emission::Mempool(mempool_txs) => {

example-crates/wallet_rpc/Cargo.toml

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[package]
2+
name = "wallet_rpc"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
8+
[dependencies]
9+
bdk = { path = "../../crates/bdk" }
10+
bdk_file_store = { path = "../../crates/file_store" }
11+
bdk_bitcoind_rpc = { path = "../../crates/bitcoind_rpc" }
12+
13+
anyhow = "1"
14+
clap = { version = "3.2.25", features = ["derive", "env"] }
15+
ctrlc = "2.0.1"

example-crates/wallet_rpc/README.md

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Wallet RPC Example
2+
3+
```
4+
$ cargo run --bin wallet_rpc -- --help
5+
6+
wallet_rpc 0.1.0
7+
Bitcoind RPC example usign `bdk::Wallet`
8+
9+
USAGE:
10+
wallet_rpc [OPTIONS] <DESCRIPTOR> [CHANGE_DESCRIPTOR]
11+
12+
ARGS:
13+
<DESCRIPTOR> Wallet descriptor [env: DESCRIPTOR=]
14+
<CHANGE_DESCRIPTOR> Wallet change descriptor [env: CHANGE_DESCRIPTOR=]
15+
16+
OPTIONS:
17+
--db-path <DB_PATH>
18+
Where to store wallet data [env: BDK_DB_PATH=] [default: .bdk_wallet_rpc_example.db]
19+
20+
-h, --help
21+
Print help information
22+
23+
--network <NETWORK>
24+
Bitcoin network to connect to [env: BITCOIN_NETWORK=] [default: testnet]
25+
26+
--rpc-cookie <RPC_COOKIE>
27+
RPC auth cookie file [env: RPC_COOKIE=]
28+
29+
--rpc-pass <RPC_PASS>
30+
RPC auth password [env: RPC_PASS=]
31+
32+
--rpc-user <RPC_USER>
33+
RPC auth username [env: RPC_USER=]
34+
35+
--start-height <START_HEIGHT>
36+
Earliest block height to start sync from [env: START_HEIGHT=] [default: 481824]
37+
38+
--url <URL>
39+
RPC URL [env: RPC_URL=] [default: 127.0.0.1:8332]
40+
41+
-V, --version
42+
Print version information
43+
44+
```
45+

example-crates/wallet_rpc/src/main.rs

+182
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
use bdk::{
2+
bitcoin::{Block, Network, Transaction},
3+
wallet::Wallet,
4+
};
5+
use bdk_bitcoind_rpc::{
6+
bitcoincore_rpc::{Auth, Client, RpcApi},
7+
Emitter,
8+
};
9+
use bdk_file_store::Store;
10+
use clap::{self, Parser};
11+
use std::{path::PathBuf, sync::mpsc::sync_channel, thread::spawn, time::Instant};
12+
13+
const DB_MAGIC: &str = "bdk-rpc-wallet-example";
14+
15+
/// Bitcoind RPC example usign `bdk::Wallet`.
16+
///
17+
/// This syncs the chain block-by-block and prints the current balance, transaction count and UTXO
18+
/// count.
19+
#[derive(Parser, Debug)]
20+
#[clap(author, version, about, long_about = None)]
21+
#[clap(propagate_version = true)]
22+
pub struct Args {
23+
/// Wallet descriptor
24+
#[clap(env = "DESCRIPTOR")]
25+
pub descriptor: String,
26+
/// Wallet change descriptor
27+
#[clap(env = "CHANGE_DESCRIPTOR")]
28+
pub change_descriptor: Option<String>,
29+
/// Earliest block height to start sync from
30+
#[clap(env = "START_HEIGHT", long, default_value = "481824")]
31+
pub start_height: u32,
32+
/// Bitcoin network to connect to
33+
#[clap(env = "BITCOIN_NETWORK", long, default_value = "testnet")]
34+
pub network: Network,
35+
/// Where to store wallet data
36+
#[clap(
37+
env = "BDK_DB_PATH",
38+
long,
39+
default_value = ".bdk_wallet_rpc_example.db"
40+
)]
41+
pub db_path: PathBuf,
42+
43+
/// RPC URL
44+
#[clap(env = "RPC_URL", long, default_value = "127.0.0.1:8332")]
45+
pub url: String,
46+
/// RPC auth cookie file
47+
#[clap(env = "RPC_COOKIE", long)]
48+
pub rpc_cookie: Option<PathBuf>,
49+
/// RPC auth username
50+
#[clap(env = "RPC_USER", long)]
51+
pub rpc_user: Option<String>,
52+
/// RPC auth password
53+
#[clap(env = "RPC_PASS", long)]
54+
pub rpc_pass: Option<String>,
55+
}
56+
57+
impl Args {
58+
fn client(&self) -> anyhow::Result<Client> {
59+
Ok(Client::new(
60+
&self.url,
61+
match (&self.rpc_cookie, &self.rpc_user, &self.rpc_pass) {
62+
(None, None, None) => Auth::None,
63+
(Some(path), _, _) => Auth::CookieFile(path.clone()),
64+
(_, Some(user), Some(pass)) => Auth::UserPass(user.clone(), pass.clone()),
65+
(_, Some(_), None) => panic!("rpc auth: missing rpc_pass"),
66+
(_, None, Some(_)) => panic!("rpc auth: missing rpc_user"),
67+
},
68+
)?)
69+
}
70+
}
71+
72+
#[derive(Debug)]
73+
enum Emission {
74+
SigTerm,
75+
Block(bdk_bitcoind_rpc::BlockEvent<Block>),
76+
Mempool(Vec<(Transaction, u64)>),
77+
}
78+
79+
fn main() -> anyhow::Result<()> {
80+
let args = Args::parse();
81+
82+
let rpc_client = args.client()?;
83+
println!(
84+
"Connected to Bitcoin Core RPC at {:?}",
85+
rpc_client.get_blockchain_info().unwrap()
86+
);
87+
88+
let start_load_wallet = Instant::now();
89+
let mut wallet = Wallet::new_or_load(
90+
&args.descriptor,
91+
args.change_descriptor.as_ref(),
92+
Store::<bdk::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), args.db_path)?,
93+
args.network,
94+
)?;
95+
println!(
96+
"Loaded wallet in {}s",
97+
start_load_wallet.elapsed().as_secs_f32()
98+
);
99+
100+
let balance = wallet.get_balance();
101+
println!("Wallet balance before syncing: {} sats", balance.total());
102+
103+
let wallet_tip = wallet.latest_checkpoint();
104+
println!(
105+
"Wallet tip: {} at height {}",
106+
wallet_tip.hash(),
107+
wallet_tip.height()
108+
);
109+
110+
let (sender, receiver) = sync_channel::<Emission>(21);
111+
112+
let signal_sender = sender.clone();
113+
ctrlc::set_handler(move || {
114+
signal_sender
115+
.send(Emission::SigTerm)
116+
.expect("failed to send sigterm")
117+
});
118+
119+
let emitter_tip = wallet_tip.clone();
120+
spawn(move || -> Result<(), anyhow::Error> {
121+
let mut emitter = Emitter::new(&rpc_client, emitter_tip, args.start_height);
122+
while let Some(emission) = emitter.next_block()? {
123+
sender.send(Emission::Block(emission))?;
124+
}
125+
sender.send(Emission::Mempool(emitter.mempool()?))?;
126+
Ok(())
127+
});
128+
129+
let mut blocks_received = 0_usize;
130+
for emission in receiver {
131+
match emission {
132+
Emission::SigTerm => {
133+
println!("Sigterm received, exiting...");
134+
break;
135+
}
136+
Emission::Block(block_emission) => {
137+
blocks_received += 1;
138+
let height = block_emission.block_height();
139+
let hash = block_emission.block_hash();
140+
let connected_to = block_emission.connected_to();
141+
let start_apply_block = Instant::now();
142+
wallet.apply_block_connected_to(&block_emission.block, height, connected_to)?;
143+
wallet.commit()?;
144+
let elapsed = start_apply_block.elapsed().as_secs_f32();
145+
println!(
146+
"Applied block {} at height {} in {}s",
147+
hash, height, elapsed
148+
);
149+
}
150+
Emission::Mempool(mempool_emission) => {
151+
let start_apply_mempool = Instant::now();
152+
wallet.apply_unconfirmed_txs(mempool_emission.iter().map(|(tx, time)| (tx, *time)));
153+
wallet.commit()?;
154+
println!(
155+
"Applied unconfirmed transactions in {}s",
156+
start_apply_mempool.elapsed().as_secs_f32()
157+
);
158+
break;
159+
}
160+
}
161+
}
162+
let wallet_tip_end = wallet.latest_checkpoint();
163+
let balance = wallet.get_balance();
164+
println!(
165+
"Synced {} blocks in {}s",
166+
blocks_received,
167+
start_load_wallet.elapsed().as_secs_f32(),
168+
);
169+
println!(
170+
"Wallet tip is '{}:{}'",
171+
wallet_tip_end.height(),
172+
wallet_tip_end.hash()
173+
);
174+
println!("Wallet balance is {} sats", balance.total());
175+
println!(
176+
"Wallet has {} transactions and {} utxos",
177+
wallet.transactions().count(),
178+
wallet.list_unspent().count()
179+
);
180+
181+
Ok(())
182+
}

0 commit comments

Comments
 (0)