From 64c2fbcd5461be8c59e3148af08739d2b20e034f Mon Sep 17 00:00:00 2001 From: tompro Date: Wed, 26 Feb 2025 17:43:15 +0100 Subject: [PATCH 1/2] Allow root cert connection for LND --- Cargo.lock | 48 +++++++-- payday_node_lnd/Cargo.toml | 3 +- payday_node_lnd/src/lib.rs | 2 +- payday_node_lnd/src/lnd.rs | 182 ++++++++++++++++++++++----------- payday_node_lnd/src/wrapper.rs | 26 +++-- 5 files changed, 181 insertions(+), 80 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6107f71..b226a35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -346,7 +346,7 @@ dependencies = [ "log", "pin-project-lite", "rustls 0.23.23", - "rustls-native-certs", + "rustls-native-certs 0.8.1", "rustls-pemfile 2.2.0", "rustls-pki-types", "serde", @@ -494,6 +494,16 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.0" @@ -754,8 +764,7 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fedimint-tonic-lnd" version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df03ca33b5116de3051c1e233fe341e23b04c4913c7b16042497924559bc2a2e" +source = "git+https://github.com/tompro/tonic_lnd.git#828a7df3c6572823cb6f36f48ab90245ab42227f" dependencies = [ "hex", "http-body 0.4.6", @@ -1202,6 +1211,7 @@ dependencies = [ "http 0.2.12", "hyper 0.14.32", "rustls 0.21.12", + "rustls-native-certs 0.6.3", "tokio", "tokio-rustls 0.24.1", ] @@ -1825,6 +1835,7 @@ dependencies = [ "payday_core", "tokio", "tokio-stream", + "tracing", ] [[package]] @@ -1993,7 +2004,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ "bytes", - "heck 0.5.0", + "heck 0.4.1", "itertools", "log", "multimap", @@ -2205,6 +2216,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile 1.0.4", + "schannel", + "security-framework 2.11.1", +] + [[package]] name = "rustls-native-certs" version = "0.8.1" @@ -2214,7 +2237,7 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 3.2.0", ] [[package]] @@ -2318,6 +2341,19 @@ dependencies = [ "cc", ] +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.8.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + [[package]] name = "security-framework" version = "3.2.0" @@ -2325,7 +2361,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ "bitflags 2.8.0", - "core-foundation", + "core-foundation 0.10.0", "core-foundation-sys", "libc", "security-framework-sys", diff --git a/payday_node_lnd/Cargo.toml b/payday_node_lnd/Cargo.toml index a4ce534..78d2fbe 100644 --- a/payday_node_lnd/Cargo.toml +++ b/payday_node_lnd/Cargo.toml @@ -5,10 +5,11 @@ edition = "2021" [dependencies] payday_core = { path = "../payday_core" } -fedimint-tonic-lnd = "0.2.0" +fedimint-tonic-lnd = { git = "https://github.com/tompro/tonic_lnd.git" } async-trait.workspace = true bitcoin.workspace = true tokio-stream.workspace = true cqrs-es.workspace = true tokio.workspace = true lightning-invoice.workspace = true +tracing.workspace = true diff --git a/payday_node_lnd/src/lib.rs b/payday_node_lnd/src/lib.rs index 0d850ac..d4d58fd 100644 --- a/payday_node_lnd/src/lib.rs +++ b/payday_node_lnd/src/lib.rs @@ -27,7 +27,7 @@ pub fn to_address(addr: &str, network: Network) -> Result
{ #[async_trait] impl NodeApi for Lnd { fn node_id(&self) -> String { - self.config.node_id.to_owned() + self.node_id.to_owned() } fn supports_payment_types(&self, payment_type: PaymentType) -> bool { diff --git a/payday_node_lnd/src/lnd.rs b/payday_node_lnd/src/lnd.rs index 125c8fa..867ede2 100644 --- a/payday_node_lnd/src/lnd.rs +++ b/payday_node_lnd/src/lnd.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use async_trait::async_trait; use bitcoin::{hex::DisplayHex, Address, Network}; +use tracing::error; use crate::to_address; use fedimint_tonic_lnd::{ @@ -23,7 +24,7 @@ use payday_core::{ }, }, payment::amount::Amount, - Result, + Error, Result, }; use tokio::{sync::mpsc::Sender, task::JoinHandle}; use tokio_stream::StreamExt; @@ -35,14 +36,21 @@ const LND_SETTLED: i32 = 1; #[derive(Clone)] pub struct Lnd { - pub(super) config: LndConfig, client: LndRpcWrapper, + pub(super) node_id: String, + network: Network, } impl Lnd { pub async fn new(config: LndConfig) -> Result { let client = LndRpcWrapper::new(config.clone()).await?; - Ok(Self { config, client }) + let node_id = config.node_id(); + let network = config.network(); + Ok(Self { + client, + node_id, + network, + }) } } @@ -108,7 +116,7 @@ impl LightningInvoiceApi for Lnd { #[async_trait] impl OnChainPaymentApi for Lnd { fn validate_address(&self, address: &str) -> Result
{ - to_address(address, self.config.network) + to_address(address, self.network) } async fn estimate_fee( @@ -150,7 +158,7 @@ impl OnChainPaymentApi for Lnd { let out = outputs .iter() .flat_map(|(k, v)| { - to_address(k, self.config.network) + to_address(k, self.network) .ok() .map(|a| (a, v.to_sat() as i64)) }) @@ -195,7 +203,7 @@ impl OnChainTransactionApi for Lnd { .get_transactions(start_height, end_height) .await? .iter() - .flat_map(|tx| to_on_chain_events(tx, self.config.network, &self.config.node_id)) + .flat_map(|tx| to_on_chain_events(tx, self.network, &self.node_id)) .flatten() .collect(); Ok(result) @@ -203,12 +211,45 @@ impl OnChainTransactionApi for Lnd { } #[derive(Debug, Clone)] -pub struct LndConfig { - pub node_id: String, - pub address: String, - pub cert_path: String, - pub macaroon_file: String, - pub network: Network, +pub enum LndConfig { + /// with custom cert file and macaroon binary file + CertPath { + node_id: String, + address: String, + cert_path: String, + macaroon_file: String, + network: Network, + }, + /// with root cert and macaroon string + RootCert { + node_id: String, + address: String, + macaroon: String, + network: Network, + }, +} + +impl LndConfig { + pub fn network(&self) -> Network { + match self { + LndConfig::CertPath { network, .. } => *network, + LndConfig::RootCert { network, .. } => *network, + } + } + + pub fn node_id(&self) -> String { + match self { + LndConfig::CertPath { node_id, .. } => node_id.to_owned(), + LndConfig::RootCert { node_id, .. } => node_id.to_owned(), + } + } + + pub fn address(&self) -> String { + match self { + LndConfig::CertPath { address, .. } => address.to_owned(), + LndConfig::RootCert { address, .. } => address.to_owned(), + } + } } pub struct LndPaymentEventStream { @@ -253,31 +294,33 @@ impl OnChainTransactionStreamApi for LndPaymentEventStream { } let handle = tokio::spawn(async move { let sender = sender.clone(); - let mut lnd: Client = fedimint_tonic_lnd::connect( - config.address.to_string(), - config.cert_path.to_string(), - config.macaroon_file.to_string(), - ) - .await - .expect("Failed to connect to LND on-chain transaction stream"); - - let mut stream = lnd - .lightning() - .subscribe_transactions(GetTransactionsRequest::default()) - .await - .expect("Failed to subscribe to LND on-chain transaction events") - .into_inner() - .filter(|tx| tx.is_ok()) - .map(|tx| tx.unwrap()); - - while let Some(event) = stream.next().await { - if let Ok(events) = to_on_chain_events(&event, config.network, &config.node_id) { - for event in events { - if let Err(e) = sender.send(event).await { - println!("Failed to send on chain transaction event: {:?}", e); + let network = config.network(); + let node_id = config.node_id(); + if let Ok(mut lnd) = create_client(config.clone()).await { + let mut stream = lnd + .lightning() + .subscribe_transactions(GetTransactionsRequest::default()) + .await + .expect("Failed to subscribe to LND on-chain transaction events") + .into_inner() + .filter(|tx| tx.is_ok()) + .map(|tx| tx.unwrap()); + + while let Some(event) = stream.next().await { + if let Ok(events) = to_on_chain_events(&event, network, &node_id) { + for event in events { + if let Err(e) = sender.send(event).await { + println!("Failed to send on chain transaction event: {:?}", e); + } } } } + } else { + error!( + "Failed to connect to LND {} {}", + config.node_id(), + config.address() + ); } }); Ok(handle) @@ -295,33 +338,33 @@ impl LightningTransactionStreamApi for LndPaymentEventStream { let handle = tokio::spawn(async move { let sender = sender.clone(); - let mut lnd: Client = fedimint_tonic_lnd::connect( - config.address.to_string(), - config.cert_path.to_string(), - config.macaroon_file.to_string(), - ) - .await - .expect("Failed to connect to LND lightning transaction stream"); - - let mut stream = lnd - .lightning() - .subscribe_invoices(InvoiceSubscription { - settle_index: settle_index.unwrap_or_default(), - ..Default::default() - }) - .await - .expect("Failed to subscribe to LND lightning transaction events") - .into_inner() - .filter_map(|tx| tx.ok()); - - while let Some(event) = stream.next().await { - if event.state == LND_SETTLED { - if let Ok(event) = to_lightning_event(event, &config.node_id) { - if let Err(e) = sender.send(event).await { - println!("Failed to send lightning transaction event: {:?}", e); + if let Ok(mut lnd) = create_client(config.clone()).await { + let mut stream = lnd + .lightning() + .subscribe_invoices(InvoiceSubscription { + settle_index: settle_index.unwrap_or_default(), + ..Default::default() + }) + .await + .expect("Failed to subscribe to LND lightning transaction events") + .into_inner() + .filter_map(|tx| tx.ok()); + + while let Some(event) = stream.next().await { + if event.state == LND_SETTLED { + if let Ok(event) = to_lightning_event(event, &config.node_id()) { + if let Err(e) = sender.send(event).await { + println!("Failed to send lightning transaction event: {:?}", e); + } } } } + } else { + error!( + "Failed to connect to LND {} {}", + config.node_id(), + config.address() + ); } }); Ok(handle) @@ -395,3 +438,26 @@ fn to_on_chain_events( .collect(); Ok(res) } + +pub(crate) async fn create_client(config: LndConfig) -> Result { + let lnd: Client = match config { + LndConfig::RootCert { + address, macaroon, .. + } => fedimint_tonic_lnd::connect_root(address.to_string(), macaroon.to_string()) + .await + .map_err(|e| Error::NodeConnectError(e.to_string()))?, + LndConfig::CertPath { + address, + cert_path, + macaroon_file, + .. + } => fedimint_tonic_lnd::connect( + address.to_string(), + cert_path.to_string(), + macaroon_file.to_string(), + ) + .await + .map_err(|e| Error::NodeConnectError(e.to_string()))?, + }; + Ok(lnd) +} diff --git a/payday_node_lnd/src/wrapper.rs b/payday_node_lnd/src/wrapper.rs index c6fc3de..8ace020 100644 --- a/payday_node_lnd/src/wrapper.rs +++ b/payday_node_lnd/src/wrapper.rs @@ -6,7 +6,7 @@ //! operations needed for invoicing. use std::{collections::HashMap, str::FromStr, sync::Arc, time::Duration}; -use crate::to_address; +use crate::{lnd::create_client, to_address}; use bitcoin::{hex::DisplayHex, Address, Amount, Network, PublicKey}; use fedimint_tonic_lnd::{ lnrpc::{ @@ -25,21 +25,16 @@ use crate::lnd::LndConfig; #[derive(Clone)] pub struct LndRpcWrapper { - config: LndConfig, client: Arc>, + node_id: String, + network: Network, } impl LndRpcWrapper { /// Create a new LND RPC wrapper. Creates an RPC connection and /// checks whether the RPC server is serving the expected network. pub async fn new(config: LndConfig) -> Result { - let mut lnd: Client = fedimint_tonic_lnd::connect( - config.address.to_string(), - config.cert_path.to_string(), - config.macaroon_file.to_string(), - ) - .await - .map_err(|e| Error::NodeConnectError(e.to_string()))?; + let mut lnd = create_client(config.clone()).await?; let network_info = lnd .lightning() @@ -53,19 +48,22 @@ impl LndRpcWrapper { .network .to_string(); - if config.network != network_from_str(&network_info)? { + let node_id = config.node_id(); + let network = config.network(); + if network != network_from_str(&network_info)? { return Err(Error::InvalidBitcoinNetwork(network_info)); } Ok(Self { - config, client: Arc::new(Mutex::new(lnd)), + node_id, + network, }) } /// Get the unique name of the LND server. Names are used to /// identify the server in logs and associated addresses and invoices. pub fn get_name(&self) -> String { - self.config.node_id.to_string() + self.node_id.to_string() } async fn client(&self) -> MutexGuard { @@ -113,7 +111,7 @@ impl LndRpcWrapper { .map_err(|e| Error::NodeApiError(e.to_string()))? .into_inner() .address; - let address = to_address(&addr, self.config.network)?; + let address = to_address(&addr, self.network)?; Ok(address) } @@ -125,7 +123,7 @@ impl LndRpcWrapper { address: &str, sats_per_vbyte: Amount, ) -> Result { - let checked_address = to_address(address, self.config.network)?; + let checked_address = to_address(address, self.network)?; let txid = self .client() .await From c12720b1382c6075bfc1d74ecf173cd818193ae9 Mon Sep 17 00:00:00 2001 From: tompro Date: Thu, 20 Mar 2025 20:34:50 +0100 Subject: [PATCH 2/2] Add trait for lnd client API --- payday_node_lnd/src/lnd.rs | 18 ++++-- payday_node_lnd/src/wrapper.rs | 115 ++++++++++++++++++++++++++++----- 2 files changed, 112 insertions(+), 21 deletions(-) diff --git a/payday_node_lnd/src/lnd.rs b/payday_node_lnd/src/lnd.rs index ee76ddd..9184051 100644 --- a/payday_node_lnd/src/lnd.rs +++ b/payday_node_lnd/src/lnd.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc}; use async_trait::async_trait; use bitcoin::{hex::DisplayHex, Address, Network}; @@ -29,21 +29,21 @@ use payday_core::{ use tokio::{sync::mpsc::Sender, task::JoinHandle}; use tokio_stream::StreamExt; -use crate::wrapper::LndRpcWrapper; +use crate::wrapper::{LndApi, LndRpcWrapper}; // The numeric state that LND indicates a settled invoice with. const LND_SETTLED: i32 = 1; #[derive(Clone)] pub struct Lnd { - client: LndRpcWrapper, + client: Arc, pub(super) node_id: String, network: Network, } impl Lnd { pub async fn new(config: LndConfig) -> Result { - let client = LndRpcWrapper::new(config.clone()).await?; + let client = Arc::new(LndRpcWrapper::new(config.clone()).await?); let node_id = config.node_id(); let network = config.network(); Ok(Self { @@ -52,6 +52,16 @@ impl Lnd { network, }) } + + pub async fn with_lnd_api(config: LndConfig, lnd: Arc) -> Result { + let node_id = config.node_id(); + let network = config.network(); + Ok(Self { + client: lnd, + node_id, + network, + }) + } } #[async_trait] diff --git a/payday_node_lnd/src/wrapper.rs b/payday_node_lnd/src/wrapper.rs index 56c1ddd..dbfb860 100644 --- a/payday_node_lnd/src/wrapper.rs +++ b/payday_node_lnd/src/wrapper.rs @@ -7,6 +7,7 @@ use std::{collections::HashMap, str::FromStr, sync::Arc, time::Duration}; use crate::{lnd::create_client, to_address}; +use async_trait::async_trait; use bitcoin::{hex::DisplayHex, Address, Amount, Network, PublicKey}; use fedimint_tonic_lnd::{ lnrpc::{ @@ -30,6 +31,83 @@ pub struct LndRpcWrapper { network: Network, } +#[async_trait] +pub trait LndApi: Send + Sync { + /// Get the unique name of the LND server. Names are used to + /// identify the server in logs and associated addresses and invoices. + fn get_name(&self) -> String; + + async fn get_onchain_balance(&self) -> Result; + + async fn get_channel_balance(&self) -> Result; + + /// Get the current balances (onchain and lightning) of the wallet. + async fn get_balances(&self) -> Result<(WalletBalanceResponse, ChannelBalanceResponse)>; + + /// Get a new onchain address for the wallet. Address is parsed and + /// validated for the configure network. + async fn new_address(&self) -> Result
; + + /// Send coins to an address. Address is parsed and validated for the configure network. + /// Returns the transaction id. + async fn send_coins( + &self, + amount: Amount, + address: &str, + sats_per_vbyte: Amount, + ) -> Result; + + /// Send coins to multiple addresses. + async fn batch_send( + &self, + outputs: HashMap, + sats_per_vbyte: Amount, + ) -> Result; + + /// Estimate the fee for a transaction. + async fn estimate_fee(&self, target_conf: i32, outputs: HashMap) + -> Result; + + /// Creates a lightning invoice. + async fn create_invoice( + &self, + amount: Amount, + memo: Option, + ttl: Option, + ) -> Result; + + /// Pay a given bolt11 invoice. The fee limit is optional and defaults to 0 (no limit) the + /// optional timeout defaults to 60 seconds. + async fn send_lightning_payment( + &self, + request: fedimint_tonic_lnd::routerrpc::SendPaymentRequest, + ) -> Result; + + /// Pay a given bolt11 invoice. The fee limit is optional and defaults to 0 (no limit) the + /// optional timeout defaults to 60 seconds. + async fn pay_invoice( + &self, + invoice: Bolt11Invoice, + fee_limit_sat: Option, + timeout: Option, + ) -> Result; + + /// Pay a specified amount to a node id. The optional timeout defaults to 60 seconds. + async fn pay_to_node_id( + &self, + node_id: PublicKey, + amount: Amount, + timeout: Option, + ) -> Result; + + /// Get a list of onchain transactions between the given start and end heights. + async fn get_transactions( + &self, + start_height: i32, + end_height: i32, + ) -> Result>; +} + impl LndRpcWrapper { /// Create a new LND RPC wrapper. Creates an RPC connection and /// checks whether the RPC server is serving the expected network. @@ -59,17 +137,20 @@ impl LndRpcWrapper { }) } + async fn client(&self) -> MutexGuard { + self.client.lock().await + } +} + +#[async_trait] +impl LndApi for LndRpcWrapper { /// Get the unique name of the LND server. Names are used to /// identify the server in logs and associated addresses and invoices. - pub fn get_name(&self) -> String { + fn get_name(&self) -> String { self.node_id.to_string() } - async fn client(&self) -> MutexGuard { - self.client.lock().await - } - - pub async fn get_onchain_balance(&self) -> Result { + async fn get_onchain_balance(&self) -> Result { let mut lnd = self.client().await; Ok(lnd .lightning() @@ -79,7 +160,7 @@ impl LndRpcWrapper { .into_inner()) } - pub async fn get_channel_balance(&self) -> Result { + async fn get_channel_balance(&self) -> Result { let mut lnd = self.client().await; Ok(lnd .lightning() @@ -90,7 +171,7 @@ impl LndRpcWrapper { } /// Get the current balances (onchain and lightning) of the wallet. - pub async fn get_balances(&self) -> Result<(WalletBalanceResponse, ChannelBalanceResponse)> { + async fn get_balances(&self) -> Result<(WalletBalanceResponse, ChannelBalanceResponse)> { let on_chain = self.get_onchain_balance().await?; let lightning = self.get_channel_balance().await?; Ok((on_chain, lightning)) @@ -98,7 +179,7 @@ impl LndRpcWrapper { /// Get a new onchain address for the wallet. Address is parsed and /// validated for the configure network. - pub async fn new_address(&self) -> Result
{ + async fn new_address(&self) -> Result
{ let addr = self .client() .await @@ -116,7 +197,7 @@ impl LndRpcWrapper { /// Send coins to an address. Address is parsed and validated for the configure network. /// Returns the transaction id. - pub async fn send_coins( + async fn send_coins( &self, amount: Amount, address: &str, @@ -142,7 +223,7 @@ impl LndRpcWrapper { } /// Send coins to multiple addresses. - pub async fn batch_send( + async fn batch_send( &self, outputs: HashMap, sats_per_vbyte: Amount, @@ -169,7 +250,7 @@ impl LndRpcWrapper { } /// Estimate the fee for a transaction. - pub async fn estimate_fee( + async fn estimate_fee( &self, target_conf: i32, outputs: HashMap, @@ -192,7 +273,7 @@ impl LndRpcWrapper { } /// Creates a lightning invoice. - pub async fn create_invoice( + async fn create_invoice( &self, amount: Amount, memo: Option, @@ -220,7 +301,7 @@ impl LndRpcWrapper { /// Pay a given bolt11 invoice. The fee limit is optional and defaults to 0 (no limit) the /// optional timeout defaults to 60 seconds. - pub async fn send_lightning_payment( + async fn send_lightning_payment( &self, request: fedimint_tonic_lnd::routerrpc::SendPaymentRequest, ) -> Result { @@ -255,7 +336,7 @@ impl LndRpcWrapper { /// Pay a given bolt11 invoice. The fee limit is optional and defaults to 0 (no limit) the /// optional timeout defaults to 60 seconds. - pub async fn pay_invoice( + async fn pay_invoice( &self, invoice: Bolt11Invoice, fee_limit_sat: Option, @@ -275,7 +356,7 @@ impl LndRpcWrapper { } /// Pay a specified amount to a node id. The optional timeout defaults to 60 seconds. - pub async fn pay_to_node_id( + async fn pay_to_node_id( &self, node_id: PublicKey, amount: Amount, @@ -294,7 +375,7 @@ impl LndRpcWrapper { } /// Get a list of onchain transactions between the given start and end heights. - pub async fn get_transactions( + async fn get_transactions( &self, start_height: i32, end_height: i32,