Skip to content

Commit 6990a67

Browse files
author
Conor Okus
committed
Adds bitcoin-rpc-client example
Update edition value Add missing dependency Downgrade Tokio version Trigger build Create seperate workspace for examples Updates Cargo.toml Build examples in ci Fix yml formatting Remove test script Update tokio version Make dependencies same as sample Add missing dependency Update script for windows Update bash script Updates shell script Adds comment Fix linting errors Trigger Build
1 parent 34cdca9 commit 6990a67

File tree

9 files changed

+336
-3
lines changed

9 files changed

+336
-3
lines changed

.github/workflows/build.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,25 +22,31 @@ jobs:
2222
- toolchain: stable
2323
build-net-tokio: true
2424
build-no-std: true
25+
build-examples: true
2526
- toolchain: stable
2627
platform: macos-latest
2728
build-net-tokio: true
2829
build-no-std: true
30+
build-examples: true
2931
- toolchain: beta
3032
platform: macos-latest
3133
build-net-tokio: true
3234
build-no-std: true
35+
build-examples: true
3336
- toolchain: stable
3437
platform: windows-latest
3538
build-net-tokio: true
3639
build-no-std: true
40+
build-examples: true
3741
- toolchain: beta
3842
platform: windows-latest
3943
build-net-tokio: true
4044
build-no-std: true
45+
build-examples: true
4146
- toolchain: beta
4247
build-net-tokio: true
4348
build-no-std: true
49+
build-examples: true
4450
- toolchain: 1.36.0
4551
build-no-std: false
4652
test-log-variants: true
@@ -195,6 +201,15 @@ jobs:
195201
# Maybe if codecov wasn't broken we wouldn't need to do this...
196202
token: f421b687-4dc2-4387-ac3d-dc3b2528af57
197203
fail_ci_if_error: true
204+
- name: Build examples
205+
if: matrix.build-examples
206+
shell: bash
207+
run: |
208+
cd examples
209+
for FILE in */; do
210+
cd $FILE
211+
cargo build --verbose --color always
212+
done
198213
199214
benchmark:
200215
runs-on: ubuntu-latest

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@ lightning-c-bindings/a.out
88
Cargo.lock
99
.idea
1010
lightning/target
11+
/examples/Cargo.lock
12+
/examples/target

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ members = [
77
"lightning-net-tokio",
88
"lightning-persister",
99
"lightning-background-processor",
10+
11+
"examples/*"
1012
]
1113

1214
# Our tests do actual crypo and lots of work, the tradeoff for -O1 is well worth it.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[package]
2+
name = "bitcoind-rpc-client"
3+
version = "0.1.0"
4+
authors = ["Conor Okus <conor@squarecrypto.org>"]
5+
edition = "2018"
6+
license = "MIT OR Apache-2.0"
7+
8+
[dependencies]
9+
lightning = { path = "../../lightning" }
10+
lightning-block-sync = { path = "../../lightning-block-sync", features = ["rpc-client"]}
11+
lightning-net-tokio = { path = "../../lightning-net-tokio" }
12+
13+
base64 = "0.13.0"
14+
bitcoin = "0.27"
15+
bitcoin-bech32 = "0.12"
16+
bech32 = "0.8"
17+
hex = "0.3"
18+
serde_json = { version = "1.0" }
19+
tokio = { version = "1.5", features = [ "io-util", "macros", "rt", "rt-multi-thread", "sync", "net", "time" ] }
20+
21+
[profile.release]
22+
panic = "abort"
23+
24+
[profile.dev]
25+
panic = "abort"
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: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
use std::convert::TryInto;
2+
3+
use bitcoin::{BlockHash, hashes::hex::FromHex};
4+
use lightning_block_sync::http::JsonResponse;
5+
6+
// TryInto implementation specifies the conversion logic from json response to BlockchainInfo object.
7+
pub struct BlockchainInfoResponse {
8+
pub latest_height: usize,
9+
pub latest_blockhash: BlockHash,
10+
pub chain: String,
11+
}
12+
13+
impl TryInto<BlockchainInfoResponse> for JsonResponse {
14+
type Error = std::io::Error;
15+
fn try_into(self) -> std::io::Result<BlockchainInfoResponse> {
16+
Ok(BlockchainInfoResponse {
17+
latest_height: self.0["blocks"].as_u64().unwrap() as usize,
18+
latest_blockhash: BlockHash::from_hex(self.0["bestblockhash"].as_str().unwrap())
19+
.unwrap(),
20+
chain: self.0["chain"].as_str().unwrap().to_string(),
21+
})
22+
}
23+
}
24+
25+
pub struct CreateWalletResponse {
26+
pub name: String,
27+
pub warning: String,
28+
}
29+
30+
impl TryInto<CreateWalletResponse> for JsonResponse {
31+
type Error = std::io::Error;
32+
fn try_into(self) -> std::io::Result<CreateWalletResponse> {
33+
Ok(CreateWalletResponse {
34+
name: self.0["name"].as_str().unwrap().to_string(),
35+
warning: self.0["warning"].as_str().unwrap().to_string(),
36+
})
37+
}
38+
}
39+
pub struct GetBalanceResponse(pub usize);
40+
41+
impl TryInto<GetBalanceResponse> for JsonResponse {
42+
type Error = std::io::Error;
43+
fn try_into(self) -> std::io::Result<GetBalanceResponse> {
44+
Ok(GetBalanceResponse(self.0.as_f64().unwrap() as usize))
45+
}
46+
}
47+
48+
pub struct GenerateToAddressResponse(pub Vec<BlockHash>);
49+
50+
impl TryInto<GenerateToAddressResponse> for JsonResponse {
51+
type Error = std::io::Error;
52+
fn try_into(self) -> std::io::Result<GenerateToAddressResponse> {
53+
let mut x: Vec<BlockHash> = Vec::new();
54+
55+
for item in self.0.as_array().unwrap() {
56+
x.push(BlockHash::from_hex(item.as_str().unwrap())
57+
.unwrap());
58+
}
59+
60+
Ok(GenerateToAddressResponse(x))
61+
}
62+
}
63+
64+
65+
pub struct NewAddressResponse(pub String);
66+
67+
impl TryInto<NewAddressResponse> for JsonResponse {
68+
type Error = std::io::Error;
69+
fn try_into(self) -> std::io::Result<NewAddressResponse> {
70+
Ok(NewAddressResponse(self.0.as_str().unwrap().to_string()))
71+
}
72+
}
73+
74+
75+
76+
77+
78+
79+
80+
81+
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+
}

lightning-block-sync/src/rpc.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,12 @@ impl RpcClient {
5353
match e.get_ref().unwrap().downcast_ref::<HttpError>() {
5454
Some(http_error) => match JsonResponse::try_from(http_error.contents.clone()) {
5555
Ok(JsonResponse(response)) => response,
56-
Err(_) => Err(e)?,
56+
Err(_) => return Err(e),
5757
},
58-
None => Err(e)?,
58+
None => return Err(e),
5959
}
6060
},
61-
Err(e) => Err(e)?,
61+
Err(e) => return Err(e),
6262
};
6363

6464
if !response.is_object() {

0 commit comments

Comments
 (0)