Skip to content

Commit dfd35d3

Browse files
author
Conor Okus
committed
Adds bitcoin-rpc-client example
1 parent 5655590 commit dfd35d3

File tree

6 files changed

+317
-0
lines changed

6 files changed

+317
-0
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ members = [
77
"lightning-net-tokio",
88
"lightning-persister",
99
"lightning-background-processor",
10+
"examples/*"
1011
]
1112

1213
# Our tests do actual crypo and lots of work, the tradeoff for -O1 is well worth it.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
[package]
2+
name = "bitcoind-rpc-client"
3+
version = "0.1.0"
4+
authors = ["Conor Okus <conor@squarecrypto.org>"]
5+
license = "MIT OR Apache-2.0"
6+
edition = "2021"
7+
8+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
9+
10+
[dependencies]
11+
lightning = { version = "0.0.104" }
12+
lightning-block-sync = { version = "0.0.104", features = ["rpc-client"]}
13+
lightning-net-tokio = { version = "0.0.104" }
14+
15+
base64 = { version = "0.13.0" }
16+
bitcoin = { version = "0.27.1" }
17+
bitcoin-bech32 = { version = "0.12.1" }
18+
bech32 = { version = "0.8.1" }
19+
hex = { version = "0.4.3" }
20+
21+
serde_json = { version = "1.0.73" }
22+
tokio = { version = "1.15.0", features = [ "io-util", "macros", "rt", "rt-multi-thread", "sync", "net", "time" ] }
23+
24+
[profile.release]
25+
panic = "abort"
26+
27+
[profile.dev]
28+
panic = "abort"
29+
30+
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Creates a basic bitcoind client
2+
3+
This is example shows how you could create a client that directly communicates with bitcoind from LDK. The API is flexible and allows for different ways to implement the interface.
4+
5+
It implements some basic RPC methods that allow you to create a core wallet and print it's balance to stdout.
6+
7+
To run with this example you need to have a bitcoin core node running in regtest mode. Get the bitcoin core binary either from the [bitcoin core repo](https://bitcoincore.org/bin/bitcoin-core-0.22.0/) or [build from source](https://github.com/bitcoin/bitcoin/blob/v0.21.1/doc/build-unix.md).
8+
9+
Then configure the node with the following `bitcoin.conf`
10+
11+
```
12+
regtest=1
13+
fallbackfee=0.0001
14+
server=1
15+
txindex=1
16+
rpcuser=admin
17+
rpcpassword=password
18+
```
19+
20+
## How to use
21+
22+
```
23+
Cargo run
24+
```
25+
26+
## Notes
27+
28+
`RpcClient` is a simple RPC client for calling methods using HTTP POST. It is implemented in [rust-lightning/lightning-block-sync/rpc.rs](https://github.com/lightningdevkit/rust-lightning/blob/61341df39e90de9d650851a624c0644f5c9dd055/lightning-block-sync/src/rpc.rs)
29+
30+
The purpose of `RpcClient` is to create a new RPC client connected to the given endpoint with the provided credentials. The credentials should be a base64 encoding of a user name and password joined by a colon, as is required for HTTP basic access authentication.
31+
32+
It implements [BlockSource](https://github.com/rust-bitcoin/rust-lightning/blob/61341df39e90de9d650851a624c0644f5c9dd055/lightning-block-sync/src/lib.rs#L55) against a Bitcoin Core RPC. It is an asynchronous interface for retrieving block headers and data.
33+
34+
Check out our [LDK sample node](https://github.com/lightningdevkit/ldk-sample) for an integrated example.
35+
36+
37+
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
use std::{sync::{Arc}};
2+
3+
use bitcoin::{BlockHash, Block};
4+
use lightning_block_sync::{rpc::RpcClient, http::{HttpEndpoint}, BlockSource, AsyncBlockSourceResult, BlockHeaderData};
5+
use tokio::sync::Mutex;
6+
7+
use crate::convert::{CreateWalletResponse, BlockchainInfoResponse, NewAddressResponse, GetBalanceResponse, GenerateToAddressResponse};
8+
9+
10+
pub struct BitcoindClient {
11+
bitcoind_rpc_client: Arc<Mutex<RpcClient>>,
12+
host: String,
13+
port: u16,
14+
rpc_user: String,
15+
rpc_password: String,
16+
}
17+
18+
impl BlockSource for &BitcoindClient {
19+
fn get_header<'a>(
20+
&'a mut self, header_hash: &'a BlockHash, height_hint: Option<u32>,
21+
) -> AsyncBlockSourceResult<'a, BlockHeaderData> {
22+
Box::pin(async move {
23+
let mut rpc = self.bitcoind_rpc_client.lock().await;
24+
rpc.get_header(header_hash, height_hint).await
25+
})
26+
}
27+
28+
fn get_block<'a>(
29+
&'a mut self, header_hash: &'a BlockHash,
30+
) -> AsyncBlockSourceResult<'a, Block> {
31+
Box::pin(async move {
32+
let mut rpc = self.bitcoind_rpc_client.lock().await;
33+
rpc.get_block(header_hash).await
34+
})
35+
}
36+
37+
fn get_best_block<'a>(&'a mut self) -> AsyncBlockSourceResult<(BlockHash, Option<u32>)> {
38+
Box::pin(async move {
39+
let mut rpc = self.bitcoind_rpc_client.lock().await;
40+
rpc.get_best_block().await
41+
})
42+
}
43+
}
44+
45+
impl BitcoindClient {
46+
pub async fn new(host: String, port: u16, rpc_user: String, rpc_password: String) -> std::io::Result<Self> {
47+
let http_endpoint = HttpEndpoint::for_host(host.clone()).with_port(port);
48+
let rpc_creditials =
49+
base64::encode(format!("{}:{}", rpc_user.clone(), rpc_password.clone()));
50+
let mut bitcoind_rpc_client = RpcClient::new(&rpc_creditials, http_endpoint)?;
51+
let _dummy = bitcoind_rpc_client
52+
.call_method::<BlockchainInfoResponse>("getblockchaininfo", &vec![])
53+
.await
54+
.map_err(|_| {
55+
std::io::Error::new(std::io::ErrorKind::PermissionDenied,
56+
"Failed to make initial call to bitcoind - please check your RPC user/password and access settings")
57+
})?;
58+
59+
let client = Self {
60+
bitcoind_rpc_client: Arc::new(Mutex::new(bitcoind_rpc_client)),
61+
host,
62+
port,
63+
rpc_user,
64+
rpc_password,
65+
};
66+
67+
Ok(client)
68+
}
69+
70+
pub fn get_new_rpc_client(&self) -> std::io::Result<RpcClient> {
71+
let http_endpoint = HttpEndpoint::for_host(self.host.clone()).with_port(self.port);
72+
let rpc_credentials =
73+
base64::encode(format!("{}:{}", self.rpc_user.clone(), self.rpc_password.clone()));
74+
RpcClient::new(&rpc_credentials, http_endpoint)
75+
}
76+
77+
pub async fn get_blockchain_info(&self) -> BlockchainInfoResponse {
78+
let mut rpc = self.bitcoind_rpc_client.lock().await;
79+
rpc.call_method::<BlockchainInfoResponse>("getblockchaininfo", &vec![]).await.unwrap()
80+
}
81+
82+
pub async fn create_wallet(&self) -> CreateWalletResponse {
83+
let mut rpc = self.bitcoind_rpc_client.lock().await;
84+
let create_wallet_args = vec![serde_json::json!("test-wallet")];
85+
86+
rpc.call_method::<CreateWalletResponse>("createwallet", &create_wallet_args).await.unwrap()
87+
}
88+
89+
pub async fn get_new_address(&self) -> String {
90+
let mut rpc = self.bitcoind_rpc_client.lock().await;
91+
92+
let addr_args = vec![serde_json::json!("LDK output address")];
93+
let addr = rpc.call_method::<NewAddressResponse>("getnewaddress", &addr_args).await.unwrap();
94+
addr.0.to_string()
95+
}
96+
97+
pub async fn get_balance(&self) -> GetBalanceResponse {
98+
let mut rpc = self.bitcoind_rpc_client.lock().await;
99+
100+
rpc.call_method::<GetBalanceResponse>("getbalance", &vec![]).await.unwrap()
101+
}
102+
103+
pub async fn generate_to_address(&self, block_num: u64, address: &str) -> GenerateToAddressResponse {
104+
let mut rpc = self.bitcoind_rpc_client.lock().await;
105+
106+
let generate_to_address_args = vec![serde_json::json!(block_num), serde_json::json!(address)];
107+
108+
109+
rpc.call_method::<GenerateToAddressResponse>("generatetoaddress", &generate_to_address_args).await.unwrap()
110+
}
111+
}
112+
113+
114+
115+
116+
117+
118+
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
use bitcoin::{BlockHash, hashes::hex::FromHex};
2+
use lightning_block_sync::http::JsonResponse;
3+
4+
pub struct BlockchainInfoResponse {
5+
pub latest_height: usize,
6+
pub latest_blockhash: BlockHash,
7+
pub chain: String,
8+
}
9+
10+
impl TryInto<BlockchainInfoResponse> for JsonResponse {
11+
type Error = std::io::Error;
12+
fn try_into(self) -> std::io::Result<BlockchainInfoResponse> {
13+
Ok(BlockchainInfoResponse {
14+
latest_height: self.0["blocks"].as_u64().unwrap() as usize,
15+
latest_blockhash: BlockHash::from_hex(self.0["bestblockhash"].as_str().unwrap())
16+
.unwrap(),
17+
chain: self.0["chain"].as_str().unwrap().to_string(),
18+
})
19+
}
20+
}
21+
22+
pub struct CreateWalletResponse {
23+
pub name: String,
24+
pub warning: String,
25+
}
26+
27+
impl TryInto<CreateWalletResponse> for JsonResponse {
28+
type Error = std::io::Error;
29+
fn try_into(self) -> std::io::Result<CreateWalletResponse> {
30+
Ok(CreateWalletResponse {
31+
name: self.0["name"].as_str().unwrap().to_string(),
32+
warning: self.0["warning"].as_str().unwrap().to_string(),
33+
})
34+
}
35+
}
36+
pub struct GetBalanceResponse(pub usize);
37+
38+
impl TryInto<GetBalanceResponse> for JsonResponse {
39+
type Error = std::io::Error;
40+
fn try_into(self) -> std::io::Result<GetBalanceResponse> {
41+
Ok(GetBalanceResponse(self.0.as_f64().unwrap() as usize))
42+
}
43+
}
44+
45+
pub struct GenerateToAddressResponse(pub Vec<BlockHash>);
46+
47+
impl TryInto<GenerateToAddressResponse> for JsonResponse {
48+
type Error = std::io::Error;
49+
fn try_into(self) -> std::io::Result<GenerateToAddressResponse> {
50+
let mut x: Vec<BlockHash> = Vec::new();
51+
52+
for item in self.0.as_array().unwrap() {
53+
x.push(BlockHash::from_hex(item.as_str().unwrap())
54+
.unwrap());
55+
}
56+
57+
Ok(GenerateToAddressResponse(x))
58+
}
59+
}
60+
61+
62+
pub struct NewAddressResponse(pub String);
63+
64+
impl TryInto<NewAddressResponse> for JsonResponse {
65+
type Error = std::io::Error;
66+
fn try_into(self) -> std::io::Result<NewAddressResponse> {
67+
Ok(NewAddressResponse(self.0.as_str().unwrap().to_string()))
68+
}
69+
}
70+
71+
72+
73+
74+
75+
76+
77+
78+
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
pub mod bitcoind_client;
2+
3+
use std::sync::Arc;
4+
5+
use crate::bitcoind_client::BitcoindClient;
6+
7+
mod convert;
8+
9+
#[tokio::main]
10+
pub async fn main() {
11+
start_ldk().await;
12+
}
13+
14+
async fn start_ldk() {
15+
// Initialize our bitcoind client
16+
let bitcoind_client = match BitcoindClient::new(
17+
String::from("127.0.0.1"),
18+
18443,
19+
String::from("admin"),
20+
String::from("password")
21+
)
22+
.await
23+
{
24+
Ok(client) => {
25+
println!("Successfully connected to bitcoind client");
26+
Arc::new(client)
27+
},
28+
Err(e) => {
29+
println!("Failed to connect to bitcoind client: {}", e);
30+
return;
31+
}
32+
};
33+
34+
// Check we connected to the expected network
35+
let bitcoind_blockchain_info = bitcoind_client.get_blockchain_info().await;
36+
println!("Chain network: {}", bitcoind_blockchain_info.chain);
37+
println!("Latest block height: {}", bitcoind_blockchain_info.latest_height);
38+
39+
// Create a named bitcoin core wallet
40+
let bitcoind_wallet = bitcoind_client.create_wallet().await;
41+
println!("Successfully created wallet with name: {}", bitcoind_wallet.name);
42+
43+
// Generate a new address
44+
let bitcoind_new_address = bitcoind_client.get_new_address().await;
45+
println!("Address: {}", bitcoind_new_address);
46+
47+
// Generate 101 blocks and use the above address as coinbase
48+
bitcoind_client.generate_to_address(101, &bitcoind_new_address).await;
49+
50+
// Show balance
51+
let balance = bitcoind_client.get_balance().await;
52+
println!("Balance: {}", balance.0);
53+
}

0 commit comments

Comments
 (0)