diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fef042beee6..afe6e73e7e4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,25 +22,31 @@ jobs: - toolchain: stable build-net-tokio: true build-no-std: true + build-examples: true - toolchain: stable platform: macos-latest build-net-tokio: true build-no-std: true + build-examples: true - toolchain: beta platform: macos-latest build-net-tokio: true build-no-std: true + build-examples: true - toolchain: stable platform: windows-latest build-net-tokio: true build-no-std: true + build-examples: true - toolchain: beta platform: windows-latest build-net-tokio: true build-no-std: true + build-examples: true - toolchain: beta build-net-tokio: true build-no-std: true + build-examples: true - toolchain: 1.36.0 build-no-std: false test-log-variants: true @@ -195,6 +201,15 @@ jobs: # Maybe if codecov wasn't broken we wouldn't need to do this... token: f421b687-4dc2-4387-ac3d-dc3b2528af57 fail_ci_if_error: true + - name: Build examples + if: matrix.build-examples + shell: bash + run: | + cd examples + for FILE in */; do + cd $FILE + cargo build --verbose --color always + done benchmark: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index a108267c2fd..42d6c54426a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ lightning-c-bindings/a.out Cargo.lock .idea lightning/target +/examples/Cargo.lock +/examples/target diff --git a/Cargo.toml b/Cargo.toml index df32ac5d9cf..a0474162e8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,8 @@ members = [ "lightning-net-tokio", "lightning-persister", "lightning-background-processor", + + "examples/*" ] # Our tests do actual crypo and lots of work, the tradeoff for -O1 is well worth it. diff --git a/examples/bitcoind-rpc-client/Cargo.toml b/examples/bitcoind-rpc-client/Cargo.toml new file mode 100644 index 00000000000..6dd9fd6976e --- /dev/null +++ b/examples/bitcoind-rpc-client/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "bitcoind-rpc-client" +version = "0.1.0" +authors = ["Conor Okus "] +edition = "2018" +license = "MIT OR Apache-2.0" + +[dependencies] +lightning = { path = "../../lightning" } +lightning-block-sync = { path = "../../lightning-block-sync", features = ["rpc-client"]} +lightning-net-tokio = { path = "../../lightning-net-tokio" } + +base64 = "0.13.0" +bitcoin = "0.27" +bitcoin-bech32 = "0.12" +bech32 = "0.8" +hex = "0.3" +serde_json = { version = "1.0" } +tokio = { version = "1.5", features = [ "io-util", "macros", "rt", "rt-multi-thread", "sync", "net", "time" ] } + +[profile.release] +panic = "abort" + +[profile.dev] +panic = "abort" \ No newline at end of file diff --git a/examples/bitcoind-rpc-client/README.md b/examples/bitcoind-rpc-client/README.md new file mode 100644 index 00000000000..9ab0777b847 --- /dev/null +++ b/examples/bitcoind-rpc-client/README.md @@ -0,0 +1,33 @@ +# Wallet actions with an RPC client + +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. + +It implements some basic RPC methods that allow you to create a wallet and print it's balance to stdout. + +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). + +Then configure the node with the following `bitcoin.conf` + +``` +regtest=1 +fallbackfee=0.0001 +server=1 +txindex=1 +rpcuser=admin +rpcpassword=password +``` + +## How to use + +``` +Cargo run +``` + +## Notes + +`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) + +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. + + + diff --git a/examples/bitcoind-rpc-client/src/bitcoind_client.rs b/examples/bitcoind-rpc-client/src/bitcoind_client.rs new file mode 100644 index 00000000000..74abbb499e6 --- /dev/null +++ b/examples/bitcoind-rpc-client/src/bitcoind_client.rs @@ -0,0 +1,84 @@ +use std::{sync::{Arc}, io}; + +use lightning_block_sync::{rpc::RpcClient, http::{HttpEndpoint}}; +use serde_json::json; +use tokio::sync::Mutex; + +use crate::convert::{CreateWalletResponse, BlockchainInfoResponse, NewAddressResponse, GetBalanceResponse, GenerateToAddressResponse}; + + +pub struct BitcoindClient { + bitcoind_rpc_client: Arc>, + host: String, + port: u16, + rpc_user: String, + rpc_password: String, +} + +impl BitcoindClient { + pub async fn new(host: String, port: u16, rpc_user: String, rpc_password: String) -> io::Result { + let http_endpoint = HttpEndpoint::for_host(host.clone()).with_port(port); + let rpc_creditials = + base64::encode(format!("{}:{}", rpc_user.clone(), rpc_password.clone())); + let mut bitcoind_rpc_client = RpcClient::new(&rpc_creditials, http_endpoint)?; + bitcoind_rpc_client + .call_method::("getblockchaininfo", &vec![]) + .await + .map_err(|_| { + io::Error::new(io::ErrorKind::PermissionDenied, + "Failed to make initial call to bitcoind - please check your RPC user/password and access settings") + })?; + + let client = Self { + bitcoind_rpc_client: Arc::new(Mutex::new(bitcoind_rpc_client)), + host, + port, + rpc_user, + rpc_password, + }; + + Ok(client) + } + + pub fn get_new_rpc_client(&self) -> io::Result { + let http_endpoint = HttpEndpoint::for_host(self.host.clone()).with_port(self.port); + let rpc_credentials = + base64::encode(format!("{}:{}", self.rpc_user.clone(), self.rpc_password.clone())); + RpcClient::new(&rpc_credentials, http_endpoint) + } + + pub async fn get_blockchain_info(&self) -> BlockchainInfoResponse { + let mut rpc = self.bitcoind_rpc_client.lock().await; + rpc.call_method::("getblockchaininfo", &vec![]).await.unwrap() + } + + pub async fn create_wallet(&self) -> CreateWalletResponse { + let mut rpc = self.bitcoind_rpc_client.lock().await; + let create_wallet_args = vec![json!("test-wallet")]; + + rpc.call_method::("createwallet", &create_wallet_args).await.unwrap() + } + + pub async fn get_new_address(&self) -> String { + let mut rpc = self.bitcoind_rpc_client.lock().await; + + let addr_args = vec![json!("LDK output address")]; + let addr = rpc.call_method::("getnewaddress", &addr_args).await.unwrap(); + addr.0.to_string() + } + + pub async fn get_balance(&self) -> GetBalanceResponse { + let mut rpc = self.bitcoind_rpc_client.lock().await; + + rpc.call_method::("getbalance", &vec![]).await.unwrap() + } + + pub async fn generate_to_address(&self, block_num: u64, address: &str) -> GenerateToAddressResponse { + let mut rpc = self.bitcoind_rpc_client.lock().await; + + let generate_to_address_args = vec![json!(block_num), json!(address)]; + + + rpc.call_method::("generatetoaddress", &generate_to_address_args).await.unwrap() + } +} \ No newline at end of file diff --git a/examples/bitcoind-rpc-client/src/convert.rs b/examples/bitcoind-rpc-client/src/convert.rs new file mode 100644 index 00000000000..a2192dd75fe --- /dev/null +++ b/examples/bitcoind-rpc-client/src/convert.rs @@ -0,0 +1,73 @@ +use std::convert::TryInto; + +use bitcoin::{Amount, BlockHash, hashes::hex::FromHex}; +use lightning_block_sync::http::JsonResponse; + +/// TryInto implementation specifies the conversion logic from json response to BlockchainInfo object. +pub struct BlockchainInfoResponse { + pub latest_height: usize, + pub latest_blockhash: BlockHash, + pub chain: String, +} + +impl TryInto for JsonResponse { + type Error = std::io::Error; + fn try_into(self) -> std::io::Result { + Ok(BlockchainInfoResponse { + latest_height: self.0["blocks"].as_u64().unwrap() as usize, + latest_blockhash: BlockHash::from_hex(self.0["bestblockhash"].as_str().unwrap()) + .unwrap(), + chain: self.0["chain"].as_str().unwrap().to_string(), + }) + } +} + +pub struct CreateWalletResponse { + pub name: String, + pub warning: String, +} + +impl TryInto for JsonResponse { + type Error = std::io::Error; + fn try_into(self) -> std::io::Result { + Ok(CreateWalletResponse { + name: self.0["name"].as_str().unwrap().to_string(), + warning: self.0["warning"].as_str().unwrap().to_string(), + }) + } +} +pub struct GetBalanceResponse(pub Amount); + +impl TryInto for JsonResponse { + type Error = std::io::Error; + fn try_into(self) -> std::io::Result { + let balance = Amount::from_btc(self.0.as_f64().unwrap()).unwrap(); + Ok(GetBalanceResponse(balance)) + } +} + +pub struct GenerateToAddressResponse(pub Vec); + +impl TryInto for JsonResponse { + type Error = std::io::Error; + fn try_into(self) -> std::io::Result { + let mut x: Vec = Vec::new(); + + for item in self.0.as_array().unwrap() { + x.push(BlockHash::from_hex(item.as_str().unwrap()) + .unwrap()); + } + + Ok(GenerateToAddressResponse(x)) + } +} + + +pub struct NewAddressResponse(pub String); + +impl TryInto for JsonResponse { + type Error = std::io::Error; + fn try_into(self) -> std::io::Result { + Ok(NewAddressResponse(self.0.as_str().unwrap().to_string())) + } +} \ No newline at end of file diff --git a/examples/bitcoind-rpc-client/src/main.rs b/examples/bitcoind-rpc-client/src/main.rs new file mode 100644 index 00000000000..f203d89b169 --- /dev/null +++ b/examples/bitcoind-rpc-client/src/main.rs @@ -0,0 +1,53 @@ +pub mod bitcoind_client; + +use std::{sync::Arc}; + +use crate::bitcoind_client::BitcoindClient; + +mod convert; + +#[tokio::main] +pub async fn main() { + start_ldk().await; +} + +async fn start_ldk() { + // Initialize our bitcoind client + let bitcoind_client = match BitcoindClient::new( + String::from("127.0.0.1"), + 18443, + String::from("admin"), + String::from("password") + ) + .await + { + Ok(client) => { + println!("Successfully connected to bitcoind client"); + Arc::new(client) + }, + Err(e) => { + println!("Failed to connect to bitcoind client: {}", e); + return; + } + }; + + // Check we connected to the expected network + let bitcoind_blockchain_info = bitcoind_client.get_blockchain_info().await; + println!("Chain network: {}", bitcoind_blockchain_info.chain); + println!("Latest block height: {}", bitcoind_blockchain_info.latest_height); + + // Create a named bitcoin core wallet + let bitcoind_wallet = bitcoind_client.create_wallet().await; + println!("Successfully created wallet with name: {}", bitcoind_wallet.name); + + // Generate a new address + let bitcoind_new_address = bitcoind_client.get_new_address().await; + println!("Address: {}", bitcoind_new_address); + + // Generate 101 blocks and use the above address as coinbase + bitcoind_client.generate_to_address(101, &bitcoind_new_address).await; + + // Show balance + let balance = bitcoind_client.get_balance().await; + println!("Balance: {}", balance.0); +} \ No newline at end of file diff --git a/lightning-block-sync/src/rpc.rs b/lightning-block-sync/src/rpc.rs index 88199688aef..15a58461642 100644 --- a/lightning-block-sync/src/rpc.rs +++ b/lightning-block-sync/src/rpc.rs @@ -53,12 +53,12 @@ impl RpcClient { match e.get_ref().unwrap().downcast_ref::() { Some(http_error) => match JsonResponse::try_from(http_error.contents.clone()) { Ok(JsonResponse(response)) => response, - Err(_) => Err(e)?, + Err(_) => return Err(e), }, - None => Err(e)?, + None => return Err(e), } }, - Err(e) => Err(e)?, + Err(e) => return Err(e), }; if !response.is_object() {