From e990a6cde2f10d725f3442a321cc0c2aebc5cbd3 Mon Sep 17 00:00:00 2001 From: Ignacio Porte Date: Fri, 19 Sep 2025 13:14:49 -0300 Subject: [PATCH 1/3] chore: use machine error codes in cli Unify error handling across CLI commands by introducing consistent error message formatting and using machine-readable codes --- src/cli.rs | 99 ++++++++++++++++++--------------------------- src/offers/mod.rs | 63 +++++++++++++---------------- src/offers/parse.rs | 10 ++--- src/server.rs | 7 ++-- 4 files changed, 75 insertions(+), 104 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index ac28fee..2b00350 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -15,6 +15,7 @@ use std::path::PathBuf; use std::process::exit; use tonic::transport::{Certificate, Channel, ClientTlsConfig}; use tonic::Request; +use tonic_types::StatusExt; fn get_macaroon_path_default(network: &str) -> PathBuf { home::home_dir() @@ -178,11 +179,7 @@ async fn main() { println!("Decoded offer: {:?}.", offer) } Err(e) => { - println!( - "ERROR please provide offer starting with lno. Provided offer is \ - invalid, failed to decode with error: {:?}.", - e - ); + println!("ERROR ({}): {}", e.code(), e); exit(1) } } @@ -196,11 +193,7 @@ async fn main() { println!("Decoded invoice: {:?}.", invoice); } Err(e) => { - println!( - "ERROR please provide hex-encoded invoice string. Provided invoice is \ - invalid, failed to decode with error: {:?}.", - e - ); + println!("ERROR ({}): {}", e.code(), e); exit(1); } } @@ -235,14 +228,10 @@ async fn main() { let mut client = OffersClient::new(channel); - let offer = match decode(offer_string.to_owned()) { + let offer = match decode(offer_string.to_string()) { Ok(offer) => offer, Err(e) => { - println!( - "ERROR: please provide offer starting with lno. Provided offer is \ - invalid, failed to decode with error: {:?}.", - e - ); + println!("ERROR ({}): {}", e.code(), e); exit(1) } }; @@ -261,10 +250,7 @@ async fn main() { match client.pay_offer(request).await { Ok(_) => println!("Successfully paid for offer!"), - Err(err) => { - println!("Error paying for offer: {err:?}"); - exit(1) - } + Err(err) => print_grpc_error(err), }; } Commands::GetInvoice { @@ -278,30 +264,26 @@ async fn main() { let grpc_port = args.grpc_port; let channel = Channel::from_shared(format!("{grpc_host}:{grpc_port}")) .unwrap_or_else(|e| { - println!("ERROR creating endpoint: {e:?}"); + println!("ERROR: failed to create endpoint {e:?}"); exit(1) }) .tls_config(tls) .unwrap_or_else(|e| { - println!("ERROR tls config: {e:?}"); + println!("ERROR: failed to configure tls {e:?}"); exit(1) }) .connect() .await .unwrap_or_else(|e| { - println!("ERROR connecting: {e:?}"); + println!("ERROR: failed to connect {e:?}"); exit(1) }); let mut client = OffersClient::new(channel); - let offer = match decode(offer_string.to_owned()) { + let offer = match decode(offer_string.to_string()) { Ok(offer) => offer, Err(e) => { - println!( - "ERROR: please provide offer starting with lno. Provided offer is \ - invalid, failed to decode with error: {:?}.", - e - ); + println!("ERROR ({}): {}", e.code(), e); exit(1) } }; @@ -316,13 +298,8 @@ async fn main() { }); add_metadata(&mut request, macaroon).unwrap_or_else(|_| exit(1)); match client.get_invoice(request).await { - Ok(response) => { - println!("Invoice: {:?}.", response.get_ref()) - } - Err(err) => { - println!("Error getting invoice for offer: {err:?}"); - exit(1) - } + Ok(response) => println!("Invoice: {:?}.", response.get_ref()), + Err(err) => print_grpc_error(err), } } Commands::PayInvoice { @@ -336,18 +313,18 @@ async fn main() { let grpc_port = args.grpc_port; let channel = Channel::from_shared(format!("{grpc_host}:{grpc_port}")) .unwrap_or_else(|e| { - println!("ERROR creating endpoint: {e:?}"); + println!("ERROR: failed to create endpoint {e:?}"); exit(1) }) .tls_config(tls) .unwrap_or_else(|e| { - println!("ERROR tls config: {e:?}"); + println!("ERROR: failed to configure tls {e:?}"); exit(1) }) .connect() .await .unwrap_or_else(|e| { - println!("ERROR connecting: {e:?}"); + println!("ERROR: failed to connect {e:?}"); exit(1) }); @@ -363,10 +340,7 @@ async fn main() { add_metadata(&mut request, macaroon).unwrap_or_else(|_| exit(1)); match client.pay_invoice(request).await { Ok(_) => println!("Successfully paid for offer!"), - Err(err) => { - println!("Error paying invoice: {err:?}"); - exit(1) - } + Err(err) => print_grpc_error(err), } } Commands::CreateOffer { @@ -381,18 +355,18 @@ async fn main() { let grpc_port = args.grpc_port; let channel = Channel::from_shared(format!("{grpc_host}:{grpc_port}")) .unwrap_or_else(|e| { - println!("ERROR creating endpoint: {e:?}"); + println!("ERROR: failed to create endpoint {e:?}"); exit(1) }) .tls_config(tls) .unwrap_or_else(|e| { - println!("ERROR tls config: {e:?}"); + println!("ERROR: failed to configure tls {e:?}"); exit(1) }) .connect() .await .unwrap_or_else(|e| { - println!("ERROR connecting: {e:?}"); + println!("ERROR: failed to connect {e:?}"); exit(1) }); @@ -408,13 +382,8 @@ async fn main() { }); add_metadata(&mut request, macaroon).unwrap_or_else(|_| exit(1)); match client.create_offer(request).await { - Ok(response) => { - println!("Offer: {:?}.", response.get_ref()) - } - Err(err) => { - println!("Error creating offer: {err:?}"); - exit(1) - } + Ok(response) => println!("Offer: {:?}.", response.get_ref()), + Err(err) => print_grpc_error(err), } } } @@ -422,7 +391,7 @@ async fn main() { fn add_metadata(request: &mut Request, macaroon: String) -> Result<(), ()> { let macaroon = macaroon.parse().map_err(|e| { - println!("Error parsing provided macaroon string into tonic metadata {e:?}") + println!("ERROR: failed to parse provided macaroon string into tonic metadata {e:?}") })?; request.metadata_mut().insert("macaroon", macaroon); @@ -450,7 +419,7 @@ fn read_cert_from_args( let pem = match (&cert_pem, &cert_path) { (Some(pem), _) => pem.clone(), (None, Some(cert_path)) => std::fs::read_to_string(cert_path) - .map_err(|e| format!("ERROR reading cert: {:?}", e))?, + .map_err(|e| format!("ERROR: failed to read cert: {:?}", e))?, (None, None) => { // If no cert pem string is provided, we'll look for the tls certificate in the // default location. @@ -459,7 +428,7 @@ fn read_cert_from_args( .join(DEFAULT_LNDK_DIR) .join(DEFAULT_DATA_DIR); std::fs::read_to_string(data_dir.join(TLS_CERT_FILENAME)) - .map_err(|e| format!("ERROR reading cert: {:?}", e))? + .map_err(|e| format!("ERROR: failed to read cert: {:?}", e))? } }; let cert = Certificate::from_pem(pem); @@ -496,7 +465,7 @@ fn read_macaroon_from_args( // set, use the default macaroon path. match macaroon_path { Some(path) => read_macaroon_from_file(path.clone()).unwrap_or_else(|e| { - println!("ERROR reading macaroon from file {e:?}"); + println!("ERROR: failed to read macaroon from file {e:?}"); exit(1) }), None => match &macaroon_hex { @@ -504,7 +473,7 @@ fn read_macaroon_from_args( None => { let path = get_macaroon_path_default(network); read_macaroon_from_file(path).unwrap_or_else(|e| { - println!("ERROR reading macaroon from file {e:?}"); + println!("ERROR: failed to read macaroon from file {e:?}"); exit(1) }) } @@ -512,6 +481,16 @@ fn read_macaroon_from_args( } } +fn print_grpc_error(err: tonic::Status) { + let details = err.get_error_details(); + if let Some(error_info) = details.error_info() { + println!("ERROR ({}): {}", error_info.reason, err.message()); + } else { + println!("ERROR: {}", err.message()); + } + exit(1) +} + #[cfg(test)] mod tests { use super::*; @@ -568,7 +547,9 @@ mod tests { let result = read_cert_from_args(None, Some(invalid_path)); assert!(result.is_err()); - assert!(result.unwrap_err().starts_with("ERROR reading cert:")); + assert!(result + .unwrap_err() + .starts_with("ERROR: failed to read cert:")); } #[test] diff --git a/src/offers/mod.rs b/src/offers/mod.rs index 07c8dae..f570ad0 100644 --- a/src/offers/mod.rs +++ b/src/offers/mod.rs @@ -12,7 +12,7 @@ use tonic_types::{ErrorDetails, StatusExt}; mod client_impls; pub mod handler; mod lnd_requests; -mod parse; +pub mod parse; pub(crate) use lnd_requests::connect_to_peer_with_retry; pub use lnd_requests::create_reply_path_for_offer_creation; @@ -131,53 +131,44 @@ impl Display for OfferError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { OfferError::AlreadyProcessing(id) => { - write!( - f, - "We're already trying to pay for a payment with this id {id}" - ) + write!(f, "Payment with id {id} already in progress") } - OfferError::BuildUIRFailure(e) => write!(f, "Error building invoice request: {e:?}"), - OfferError::SignError(e) => write!(f, "Error signing invoice request: {e:?}"), - OfferError::DeriveKeyFailure(e) => write!(f, "Error signing invoice request: {e:?}"), - OfferError::InvalidAmount(e) => write!(f, "User provided an invalid amount: {e:?}"), - OfferError::InvalidCurrency => write!( - f, - "LNDK doesn't yet support offer currencies other than bitcoin" - ), - OfferError::PeerConnectError(e) => write!(f, "Error connecting to peer: {e:?}"), - OfferError::NodeAddressNotFound => write!(f, "Couldn't get node address"), - OfferError::ListPeersFailure(e) => write!(f, "Error listing peers: {e:?}"), - OfferError::BuildBlindedPathFailure => write!(f, "Error building blinded path"), - OfferError::RouteFailure(e) => write!(f, "Error routing payment: {e:?}"), - OfferError::TrackFailure(e) => write!(f, "Error tracking payment: {e:?}"), - OfferError::PaymentFailure => write!(f, "Failed to send payment"), - OfferError::InvoiceTimeout(e) => write!(f, "Did not receive invoice in {e:?} seconds."), - OfferError::IntroductionNodeNotFound => write!(f, "Could not find introduction node."), - OfferError::GetChannelInfo(e) => write!(f, "Could not fetch channel info: {e:?}"), - OfferError::CreateOfferFailure(e) => write!(f, "Could not create offer: {e:?}"), - OfferError::CreateOfferTimeFailure => write!( - f, - "Could not create offer with expiry time given system clock" - ), + OfferError::BuildUIRFailure(e) => write!(f, "Failed to build invoice request: {e:?}"), + OfferError::SignError(e) => write!(f, "Failed to sign invoice request: {e:?}"), + OfferError::DeriveKeyFailure(e) => write!(f, "Failed to derive key: {e:?}"), + OfferError::InvalidAmount(e) => write!(f, "Invalid amount: {e:?}"), + OfferError::InvalidCurrency => write!(f, "Only bitcoin currency is supported"), + OfferError::PeerConnectError(e) => write!(f, "Failed to connect to peer: {e:?}"), + OfferError::NodeAddressNotFound => write!(f, "Node address not found"), + OfferError::ListPeersFailure(e) => write!(f, "Failed to list peers: {e:?}"), + OfferError::BuildBlindedPathFailure => write!(f, "Failed to build blinded path"), + OfferError::RouteFailure(e) => write!(f, "Failed to route payment: {e:?}"), + OfferError::TrackFailure(e) => write!(f, "Failed to track payment: {e:?}"), + OfferError::PaymentFailure => write!(f, "Payment failed"), + OfferError::InvoiceTimeout(e) => { + write!(f, "Invoice request timed out after {e:?} seconds") + } + OfferError::IntroductionNodeNotFound => write!(f, "Introduction node not found"), + OfferError::GetChannelInfo(e) => write!(f, "Failed to get channel info: {e:?}"), + OfferError::CreateOfferFailure(e) => write!(f, "Failed to create offer: {e:?}"), + OfferError::CreateOfferTimeFailure => write!(f, "Invalid offer expiry time"), OfferError::AddInvoiceFailure(e) => { - write!(f, "Could not add invoice to lnd node: {e:?}") + write!(f, "Failed to add invoice to lnd node: {e:?}") } OfferError::DecodePaymentRequestFailure(e) => { - write!(f, "Could not decode payment request: {e:?}") + write!(f, "Failed to decode payment request: {e:?}") } OfferError::ParsePaymentHashFailure(e) => { - write!(f, "Could not parse payment hash: {e:?}") + write!(f, "Failed to parse payment hash: {e:?}") } OfferError::ParseOfferFailure(e) => { - write!(f, "The provided offer was invalid. Please provide a valid offer in bech32 format, i.e. starting with 'lno'. Error: {e:?}") + write!(f, "Invalid offer: must start with 'lno'. Error: {e:?}") } OfferError::ParseInvoiceFailure(e) => { - write!(f, "The provided invoice was invalid. Please provide a valid invoice in hex format. Error: {e:?}") - } - OfferError::EncodeInvoiceFailure(e) => { - write!(f, "Failed to encode invoice to hex format. Error: {e:?}") + write!(f, "Invalid invoice: must be hex format. Error: {e:?}") } OfferError::ListChannelsFailure(e) => write!(f, "Error listing channels: {e:?}"), + OfferError::EncodeInvoiceFailure(e) => write!(f, "Failed to encode invoice: {e:?}"), } } } diff --git a/src/offers/parse.rs b/src/offers/parse.rs index 2b95f75..de6bbb9 100644 --- a/src/offers/parse.rs +++ b/src/offers/parse.rs @@ -1,16 +1,14 @@ use lightning::{ - offers::{ - offer::{Amount, Offer}, - parse::Bolt12ParseError, - }, + offers::offer::{Amount, Offer}, onion_message::messenger::Destination, }; +use std::str::FromStr; use super::OfferError; /// Decodes a bech32 offer string into an LDK offer. -pub fn decode(offer_str: String) -> Result { - offer_str.parse::() +pub fn decode(offer_str: String) -> Result { + Offer::from_str(&offer_str).map_err(OfferError::ParseOfferFailure) } /// Get the destination of an offer. diff --git a/src/server.rs b/src/server.rs index 136fe16..e06f451 100644 --- a/src/server.rs +++ b/src/server.rs @@ -2,6 +2,7 @@ use crate::lnd::{get_lnd_client, get_network, Creds, LndCfg, LndError}; use crate::lndkrpc::{CreateOfferRequest, CreateOfferResponse}; use crate::offers::get_destination; use crate::offers::handler::{CreateOfferParams, PayOfferParams}; +use crate::offers::parse::decode; use crate::offers::validate_amount; use crate::offers::OfferError; use crate::{lndkrpc, Bolt12InvoiceString, OfferHandler}; @@ -10,7 +11,7 @@ use lightning::blinded_path::payment::BlindedPaymentPath; use lightning::blinded_path::{Direction, IntroductionNode}; use lightning::ln::channelmanager::PaymentId; use lightning::offers::invoice::Bolt12Invoice; -use lightning::offers::offer::{Offer, Quantity}; +use lightning::offers::offer::Quantity; use lightning::sign::EntropySource; use lightning::util::ser::Writeable; use lndkrpc::offers_server::Offers; @@ -71,7 +72,7 @@ impl Offers for LNDKServer { let mut client = get_lnd_client(lnd_cfg)?; let inner_request = request.get_ref(); - let offer = Offer::from_str(&inner_request.offer).map_err(OfferError::ParseOfferFailure)?; + let offer = decode(inner_request.offer.clone())?; let destination = get_destination(&offer).await?; let reply_path = None; @@ -139,7 +140,7 @@ impl Offers for LNDKServer { let mut client = get_lnd_client(lnd_cfg)?; let inner_request = request.get_ref(); - let offer = Offer::from_str(&inner_request.offer).map_err(OfferError::ParseOfferFailure)?; + let offer = decode(inner_request.offer.clone())?; let destination = get_destination(&offer).await?; let reply_path = None; From efe568855248908870a83a3c5ea40f1f9775b14f Mon Sep 17 00:00:00 2001 From: Ignacio Porte Date: Fri, 19 Sep 2025 13:26:31 -0300 Subject: [PATCH 2/3] refactor(cli): extract repeated gRPC client setup and metadata handling Extracted duplicated gRPC channel creation, client authentication, and metadata handling code into reusable functions to reduce code duplication and improve error handling. --- src/cli.rs | 204 ++++++++++++++++++++--------------------------------- 1 file changed, 76 insertions(+), 128 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 2b00350..c2d6dae 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -24,6 +24,11 @@ fn get_macaroon_path_default(network: &str) -> PathBuf { .join(format!(".lnd/data/chain/bitcoin/{network}/admin.macaroon")) } +fn print_and_exit(msg: &str) -> ! { + println!("{msg}"); + exit(1) +} + /// A cli for interacting with lndk. #[derive(Debug, Parser)] #[command(name = "lndk-cli")] @@ -175,13 +180,8 @@ async fn main() { Commands::DecodeOffer { offer_string } => { println!("Decoding offer: {offer_string}."); match decode(offer_string) { - Ok(offer) => { - println!("Decoded offer: {:?}.", offer) - } - Err(e) => { - println!("ERROR ({}): {}", e.code(), e); - exit(1) - } + Ok(offer) => println!("Decoded offer: {:?}.", offer), + Err(e) => print_and_exit(&format!("ERROR ({}): {}", e.code(), e)), } } Commands::DecodeInvoice { invoice_string } => { @@ -192,10 +192,7 @@ async fn main() { Ok(invoice) => { println!("Decoded invoice: {:?}.", invoice); } - Err(e) => { - println!("ERROR ({}): {}", e.code(), e); - exit(1); - } + Err(e) => print_and_exit(&format!("ERROR ({}): {}", e.code(), e)), } } Commands::PayOffer { @@ -207,37 +204,19 @@ async fn main() { fee_limit_percent, } => { let tls = read_cert_from_args_or_exit(args.cert_pem, args.cert_path); - let grpc_host = args.grpc_host; - let grpc_port = args.grpc_port; - let channel = Channel::from_shared(format!("{grpc_host}:{grpc_port}")) - .unwrap_or_else(|e| { - println!("ERROR creating endpoint: {e:?}"); - exit(1) - }) - .tls_config(tls) - .unwrap_or_else(|e| { - println!("ERROR tls config: {e:?}"); - exit(1) - }) - .connect() - .await - .unwrap_or_else(|e| { - println!("ERROR connecting: {e:?}"); - exit(1) - }); - - let mut client = OffersClient::new(channel); + let channel = create_grpc_channel(args.grpc_host, args.grpc_port, tls).await; + let (mut client, macaroon) = create_authenticated_client( + channel, + args.macaroon_path, + args.macaroon_hex, + &args.network, + ); let offer = match decode(offer_string.to_string()) { Ok(offer) => offer, - Err(e) => { - println!("ERROR ({}): {}", e.code(), e); - exit(1) - } + Err(e) => print_and_exit(&format!("ERROR ({}): {}", e.code(), e)), }; - let macaroon = - read_macaroon_from_args(args.macaroon_path, args.macaroon_hex, &args.network); let mut request = Request::new(PayOfferRequest { offer: offer.to_string(), amount, @@ -246,7 +225,7 @@ async fn main() { fee_limit, fee_limit_percent, }); - add_metadata(&mut request, macaroon).unwrap_or_else(|_| exit(1)); + add_metadata(&mut request, macaroon); match client.pay_offer(request).await { Ok(_) => println!("Successfully paid for offer!"), @@ -260,43 +239,26 @@ async fn main() { response_invoice_timeout, } => { let tls = read_cert_from_args_or_exit(args.cert_pem, args.cert_path); - let grpc_host = args.grpc_host; - let grpc_port = args.grpc_port; - let channel = Channel::from_shared(format!("{grpc_host}:{grpc_port}")) - .unwrap_or_else(|e| { - println!("ERROR: failed to create endpoint {e:?}"); - exit(1) - }) - .tls_config(tls) - .unwrap_or_else(|e| { - println!("ERROR: failed to configure tls {e:?}"); - exit(1) - }) - .connect() - .await - .unwrap_or_else(|e| { - println!("ERROR: failed to connect {e:?}"); - exit(1) - }); + let channel = create_grpc_channel(args.grpc_host, args.grpc_port, tls).await; + let (mut client, macaroon) = create_authenticated_client( + channel, + args.macaroon_path, + args.macaroon_hex, + &args.network, + ); - let mut client = OffersClient::new(channel); let offer = match decode(offer_string.to_string()) { Ok(offer) => offer, - Err(e) => { - println!("ERROR ({}): {}", e.code(), e); - exit(1) - } + Err(e) => print_and_exit(&format!("ERROR ({}): {}", e.code(), e)), }; - let macaroon = - read_macaroon_from_args(args.macaroon_path, args.macaroon_hex, &args.network); let mut request = Request::new(GetInvoiceRequest { offer: offer.to_string(), amount, payer_note, response_invoice_timeout, }); - add_metadata(&mut request, macaroon).unwrap_or_else(|_| exit(1)); + add_metadata(&mut request, macaroon); match client.get_invoice(request).await { Ok(response) => println!("Invoice: {:?}.", response.get_ref()), Err(err) => print_grpc_error(err), @@ -309,35 +271,21 @@ async fn main() { fee_limit_percent, } => { let tls = read_cert_from_args_or_exit(args.cert_pem, args.cert_path); - let grpc_host = args.grpc_host.clone(); - let grpc_port = args.grpc_port; - let channel = Channel::from_shared(format!("{grpc_host}:{grpc_port}")) - .unwrap_or_else(|e| { - println!("ERROR: failed to create endpoint {e:?}"); - exit(1) - }) - .tls_config(tls) - .unwrap_or_else(|e| { - println!("ERROR: failed to configure tls {e:?}"); - exit(1) - }) - .connect() - .await - .unwrap_or_else(|e| { - println!("ERROR: failed to connect {e:?}"); - exit(1) - }); + let channel = create_grpc_channel(args.grpc_host, args.grpc_port, tls).await; + let (mut client, macaroon) = create_authenticated_client( + channel, + args.macaroon_path, + args.macaroon_hex, + &args.network, + ); - let mut client = OffersClient::new(channel); - let macaroon = - read_macaroon_from_args(args.macaroon_path, args.macaroon_hex, &args.network); let mut request = Request::new(PayInvoiceRequest { invoice: invoice_string.to_owned(), amount, fee_limit, fee_limit_percent, }); - add_metadata(&mut request, macaroon).unwrap_or_else(|_| exit(1)); + add_metadata(&mut request, macaroon); match client.pay_invoice(request).await { Ok(_) => println!("Successfully paid for offer!"), Err(err) => print_grpc_error(err), @@ -351,28 +299,14 @@ async fn main() { quantity, } => { let tls = read_cert_from_args_or_exit(args.cert_pem, args.cert_path); - let grpc_host = args.grpc_host.clone(); - let grpc_port = args.grpc_port; - let channel = Channel::from_shared(format!("{grpc_host}:{grpc_port}")) - .unwrap_or_else(|e| { - println!("ERROR: failed to create endpoint {e:?}"); - exit(1) - }) - .tls_config(tls) - .unwrap_or_else(|e| { - println!("ERROR: failed to configure tls {e:?}"); - exit(1) - }) - .connect() - .await - .unwrap_or_else(|e| { - println!("ERROR: failed to connect {e:?}"); - exit(1) - }); + let channel = create_grpc_channel(args.grpc_host, args.grpc_port, tls).await; + let (mut client, macaroon) = create_authenticated_client( + channel, + args.macaroon_path, + args.macaroon_hex, + &args.network, + ); - let mut client = OffersClient::new(channel); - let macaroon = - read_macaroon_from_args(args.macaroon_path, args.macaroon_hex, &args.network); let mut request = Request::new(CreateOfferRequest { amount, quantity, @@ -380,7 +314,7 @@ async fn main() { issuer, expiry, }); - add_metadata(&mut request, macaroon).unwrap_or_else(|_| exit(1)); + add_metadata(&mut request, macaroon); match client.create_offer(request).await { Ok(response) => println!("Offer: {:?}.", response.get_ref()), Err(err) => print_grpc_error(err), @@ -389,13 +323,34 @@ async fn main() { } } -fn add_metadata(request: &mut Request, macaroon: String) -> Result<(), ()> { - let macaroon = macaroon.parse().map_err(|e| { - println!("ERROR: failed to parse provided macaroon string into tonic metadata {e:?}") - })?; - request.metadata_mut().insert("macaroon", macaroon); +async fn create_grpc_channel(grpc_host: String, grpc_port: u16, tls: ClientTlsConfig) -> Channel { + Channel::from_shared(format!("{grpc_host}:{grpc_port}")) + .unwrap_or_else(|e| print_and_exit(&format!("ERROR: failed to create endpoint {e:?}"))) + .tls_config(tls) + .unwrap_or_else(|e| print_and_exit(&format!("ERROR: failed to configure tls {e:?}"))) + .connect() + .await + .unwrap_or_else(|e| print_and_exit(&format!("ERROR: failed to connect {e:?}"))) +} - Ok(()) +fn create_authenticated_client( + channel: Channel, + macaroon_path: Option, + macaroon_hex: Option, + network: &str, +) -> (OffersClient, String) { + let client = OffersClient::new(channel); + let macaroon = read_macaroon_from_args(macaroon_path, macaroon_hex, network); + (client, macaroon) +} + +fn add_metadata(request: &mut Request, macaroon: String) { + let macaroon = macaroon.parse().unwrap_or_else(|e| { + print_and_exit(&format!( + "ERROR: failed to parse provided macaroon string into tonic metadata {e:?}" + )) + }); + request.metadata_mut().insert("macaroon", macaroon); } fn read_macaroon_from_file(path: PathBuf) -> Result { @@ -443,10 +398,7 @@ fn read_cert_from_args_or_exit( ) -> ClientTlsConfig { match read_cert_from_args(cert_pem, cert_path) { Ok(config) => config, - Err(err) => { - println!("{}", err); - exit(1); - } + Err(err) => print_and_exit(&format!("ERROR: {}", err)), } } @@ -457,38 +409,34 @@ fn read_macaroon_from_args( ) -> String { // Make sure both macaroon options are not set. if macaroon_path.is_some() && macaroon_hex.is_some() { - println!("ERROR: Only one of `macaroon_path` or `macaroon_hex` should be set."); - exit(1) + print_and_exit("ERROR: Only one of `macaroon_path` or `macaroon_hex` should be set."); } // Let's grab the macaroon string now. If neither macaroon_path nor macaroon_hex are // set, use the default macaroon path. match macaroon_path { Some(path) => read_macaroon_from_file(path.clone()).unwrap_or_else(|e| { - println!("ERROR: failed to read macaroon from file {e:?}"); - exit(1) + print_and_exit(&format!("ERROR: failed to read macaroon from file {e:?}")) }), None => match &macaroon_hex { Some(macaroon) => macaroon.clone(), None => { let path = get_macaroon_path_default(network); read_macaroon_from_file(path).unwrap_or_else(|e| { - println!("ERROR: failed to read macaroon from file {e:?}"); - exit(1) + print_and_exit(&format!("ERROR: failed to read macaroon from file {e:?}")) }) } }, } } -fn print_grpc_error(err: tonic::Status) { +fn print_grpc_error(err: tonic::Status) -> ! { let details = err.get_error_details(); if let Some(error_info) = details.error_info() { - println!("ERROR ({}): {}", error_info.reason, err.message()); + print_and_exit(&format!("ERROR ({}): {}", error_info.reason, err.message())); } else { - println!("ERROR: {}", err.message()); + print_and_exit(&format!("ERROR: {}", err.message())); } - exit(1) } #[cfg(test)] From 9a214bda78507a73580e3a3a282cef70dd2785a7 Mon Sep 17 00:00:00 2001 From: Ignacio Porte Date: Wed, 24 Sep 2025 00:01:43 -0300 Subject: [PATCH 3/3] chore: replace nested LndStatus with String messages in error enums Replace LndStatus variants with String in OfferError and LndError enums, updating all error handling to extract message from LndStatus before converting to String. This simplifies error handling and provides cleaner error messages to users. --- src/cli.rs | 78 ++++++++++++++++++++++---------------- src/lnd.rs | 2 +- src/offers/client_impls.rs | 8 +++- src/offers/lnd_requests.rs | 20 +++++----- src/offers/mod.rs | 39 ++++++++++--------- src/server.rs | 6 +-- 6 files changed, 85 insertions(+), 68 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index c2d6dae..1d5b5f4 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -17,6 +17,35 @@ use tonic::transport::{Certificate, Channel, ClientTlsConfig}; use tonic::Request; use tonic_types::StatusExt; +trait ExitGracefully { + fn exit_gracefully(self) -> !; +} + +impl ExitGracefully for &str { + fn exit_gracefully(self) -> ! { + println!("{self}"); + exit(1) + } +} + +impl ExitGracefully for tonic::Status { + fn exit_gracefully(self) -> ! { + let details = self.get_error_details(); + let error_msg = if let Some(error_info) = details.error_info() { + format!("ERROR ({}): {}", error_info.reason, self.message()) + } else { + format!("ERROR: {}", self.message()) + }; + error_msg.exit_gracefully(); + } +} + +impl ExitGracefully for lndk::offers::OfferError { + fn exit_gracefully(self) -> ! { + format!("ERROR ({}): {}", self.code(), self).exit_gracefully(); + } +} + fn get_macaroon_path_default(network: &str) -> PathBuf { home::home_dir() .unwrap() @@ -24,11 +53,6 @@ fn get_macaroon_path_default(network: &str) -> PathBuf { .join(format!(".lnd/data/chain/bitcoin/{network}/admin.macaroon")) } -fn print_and_exit(msg: &str) -> ! { - println!("{msg}"); - exit(1) -} - /// A cli for interacting with lndk. #[derive(Debug, Parser)] #[command(name = "lndk-cli")] @@ -181,7 +205,7 @@ async fn main() { println!("Decoding offer: {offer_string}."); match decode(offer_string) { Ok(offer) => println!("Decoded offer: {:?}.", offer), - Err(e) => print_and_exit(&format!("ERROR ({}): {}", e.code(), e)), + Err(e) => e.exit_gracefully(), } } Commands::DecodeInvoice { invoice_string } => { @@ -192,7 +216,7 @@ async fn main() { Ok(invoice) => { println!("Decoded invoice: {:?}.", invoice); } - Err(e) => print_and_exit(&format!("ERROR ({}): {}", e.code(), e)), + Err(e) => e.exit_gracefully(), } } Commands::PayOffer { @@ -214,7 +238,7 @@ async fn main() { let offer = match decode(offer_string.to_string()) { Ok(offer) => offer, - Err(e) => print_and_exit(&format!("ERROR ({}): {}", e.code(), e)), + Err(e) => e.exit_gracefully(), }; let mut request = Request::new(PayOfferRequest { @@ -229,7 +253,7 @@ async fn main() { match client.pay_offer(request).await { Ok(_) => println!("Successfully paid for offer!"), - Err(err) => print_grpc_error(err), + Err(err) => err.exit_gracefully(), }; } Commands::GetInvoice { @@ -249,7 +273,7 @@ async fn main() { let offer = match decode(offer_string.to_string()) { Ok(offer) => offer, - Err(e) => print_and_exit(&format!("ERROR ({}): {}", e.code(), e)), + Err(e) => e.exit_gracefully(), }; let mut request = Request::new(GetInvoiceRequest { @@ -261,7 +285,7 @@ async fn main() { add_metadata(&mut request, macaroon); match client.get_invoice(request).await { Ok(response) => println!("Invoice: {:?}.", response.get_ref()), - Err(err) => print_grpc_error(err), + Err(err) => err.exit_gracefully(), } } Commands::PayInvoice { @@ -288,7 +312,7 @@ async fn main() { add_metadata(&mut request, macaroon); match client.pay_invoice(request).await { Ok(_) => println!("Successfully paid for offer!"), - Err(err) => print_grpc_error(err), + Err(err) => err.exit_gracefully(), } } Commands::CreateOffer { @@ -317,7 +341,7 @@ async fn main() { add_metadata(&mut request, macaroon); match client.create_offer(request).await { Ok(response) => println!("Offer: {:?}.", response.get_ref()), - Err(err) => print_grpc_error(err), + Err(err) => err.exit_gracefully(), } } } @@ -325,12 +349,12 @@ async fn main() { async fn create_grpc_channel(grpc_host: String, grpc_port: u16, tls: ClientTlsConfig) -> Channel { Channel::from_shared(format!("{grpc_host}:{grpc_port}")) - .unwrap_or_else(|e| print_and_exit(&format!("ERROR: failed to create endpoint {e:?}"))) + .unwrap_or_else(|e| format!("ERROR: failed to create endpoint {e:?}").exit_gracefully()) .tls_config(tls) - .unwrap_or_else(|e| print_and_exit(&format!("ERROR: failed to configure tls {e:?}"))) + .unwrap_or_else(|e| format!("ERROR: failed to configure tls {e:?}").exit_gracefully()) .connect() .await - .unwrap_or_else(|e| print_and_exit(&format!("ERROR: failed to connect {e:?}"))) + .unwrap_or_else(|e| format!("ERROR: failed to connect {e:?}").exit_gracefully()) } fn create_authenticated_client( @@ -346,9 +370,8 @@ fn create_authenticated_client( fn add_metadata(request: &mut Request, macaroon: String) { let macaroon = macaroon.parse().unwrap_or_else(|e| { - print_and_exit(&format!( - "ERROR: failed to parse provided macaroon string into tonic metadata {e:?}" - )) + format!("ERROR: failed to parse provided macaroon string into tonic metadata {e:?}") + .exit_gracefully() }); request.metadata_mut().insert("macaroon", macaroon); } @@ -398,7 +421,7 @@ fn read_cert_from_args_or_exit( ) -> ClientTlsConfig { match read_cert_from_args(cert_pem, cert_path) { Ok(config) => config, - Err(err) => print_and_exit(&format!("ERROR: {}", err)), + Err(err) => err.exit_gracefully(), } } @@ -409,36 +432,27 @@ fn read_macaroon_from_args( ) -> String { // Make sure both macaroon options are not set. if macaroon_path.is_some() && macaroon_hex.is_some() { - print_and_exit("ERROR: Only one of `macaroon_path` or `macaroon_hex` should be set."); + "ERROR: Only one of `macaroon_path` or `macaroon_hex` should be set.".exit_gracefully(); } // Let's grab the macaroon string now. If neither macaroon_path nor macaroon_hex are // set, use the default macaroon path. match macaroon_path { Some(path) => read_macaroon_from_file(path.clone()).unwrap_or_else(|e| { - print_and_exit(&format!("ERROR: failed to read macaroon from file {e:?}")) + format!("ERROR: failed to read macaroon from file {e:?}").exit_gracefully() }), None => match &macaroon_hex { Some(macaroon) => macaroon.clone(), None => { let path = get_macaroon_path_default(network); read_macaroon_from_file(path).unwrap_or_else(|e| { - print_and_exit(&format!("ERROR: failed to read macaroon from file {e:?}")) + format!("ERROR: failed to read macaroon from file {e:?}").exit_gracefully() }) } }, } } -fn print_grpc_error(err: tonic::Status) -> ! { - let details = err.get_error_details(); - if let Some(error_info) = details.error_info() { - print_and_exit(&format!("ERROR ({}): {}", error_info.reason, err.message())); - } else { - print_and_exit(&format!("ERROR: {}", err.message())); - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/lnd.rs b/src/lnd.rs index 66c84f8..3731bc3 100644 --- a/src/lnd.rs +++ b/src/lnd.rs @@ -373,7 +373,7 @@ pub enum LndError { /// LND node is not connected to bitcoin network. NetworkNotConnected, /// LND service is unavailable or not responding. - ServiceUnavailable(LndStatus), + ServiceUnavailable(String), } impl LndError { diff --git a/src/offers/client_impls.rs b/src/offers/client_impls.rs index b628f86..818d28b 100644 --- a/src/offers/client_impls.rs +++ b/src/offers/client_impls.rs @@ -189,11 +189,15 @@ impl InvoicePayer for Client { .router() .track_payment_v2(req) .await - .map_err(OfferError::TrackFailure)? + .map_err(|e| OfferError::TrackFailure(e.message().to_string()))? .into_inner(); // Wait for a failed or successful payment. - while let Some(payment) = stream.message().await.map_err(OfferError::TrackFailure)? { + while let Some(payment) = stream + .message() + .await + .map_err(|e| OfferError::TrackFailure(e.message().to_string()))? + { if payment.status() == tonic_lnd::lnrpc::payment::PaymentStatus::Succeeded { return Ok(payment); } else if payment.status() == tonic_lnd::lnrpc::payment::PaymentStatus::Failed { diff --git a/src/offers/lnd_requests.rs b/src/offers/lnd_requests.rs index 5d02bd8..643f722 100644 --- a/src/offers/lnd_requests.rs +++ b/src/offers/lnd_requests.rs @@ -122,13 +122,13 @@ pub(crate) async fn send_payment( params.fee_limit, ) .await - .map_err(OfferError::RouteFailure)?; + .map_err(|e| OfferError::RouteFailure(e.message().to_string()))?; trace!("Routes found {}...", resp.routes.len()); let resp = payer .send_to_route(params.payment_hash, resp.routes[0].clone()) .await - .map_err(OfferError::RouteFailure)?; + .map_err(|e| OfferError::RouteFailure(e.message().to_string()))?; trace!( "Sent payment using preimage {} using attempt_id {} with status {}. {}", @@ -253,13 +253,13 @@ pub(super) async fn create_invoice_info_from_request( let invoice_response = creator .add_invoice(invoice_request) .await - .map_err(OfferError::AddInvoiceFailure)?; + .map_err(|e| OfferError::AddInvoiceFailure(e.message().to_string()))?; let payment_request = invoice_response.payment_request; log::trace!("Payment request: {:?}", payment_request); let payreq = creator .decode_payment_request(payment_request) .await - .map_err(OfferError::DecodePaymentRequestFailure)?; + .map_err(|e| OfferError::DecodePaymentRequestFailure(e.message().to_string()))?; log::trace!("Decoded payment request: {:?}", payreq); let payment_hash = bitcoin::hashes::sha256::Hash::from_str(&payreq.payment_hash).map_err(|e| { @@ -290,7 +290,7 @@ pub async fn create_reply_path_for_offer_creation( // Find introduction channels for our blinded paths. let current_channels = connector.list_active_public_channels().await.map_err(|e| { error!("Could not lookup current channels: {e}."); - OfferError::ListChannelsFailure(e) + OfferError::ListChannelsFailure(e.message().to_string()) })?; let mut intro_channels = HashSet::new(); @@ -376,7 +376,7 @@ pub async fn create_reply_path_for_outgoing_payments( // Find an introduction node for our blinded path. let current_peers = connector.list_peers().await.map_err(|e| { error!("Could not lookup current peers: {e}."); - OfferError::ListPeersFailure(e) + OfferError::ListPeersFailure(e.message().to_string()) })?; let mut intro_node = None; @@ -535,7 +535,7 @@ pub(crate) async fn connect_to_peer( let resp = connector .list_peers() .await - .map_err(OfferError::PeerConnectError)?; + .map_err(|e| OfferError::PeerConnectError(e.message().to_string()))?; let node_id_str = node_id.to_string(); for peer in resp.peers.iter() { @@ -551,7 +551,7 @@ pub(crate) async fn connect_to_peer( let node = connector .get_node_info(node_id_str.clone(), false) .await - .map_err(OfferError::PeerConnectError)?; + .map_err(|e| OfferError::PeerConnectError(e.message().to_string()))?; let node = match node.node { Some(node) => node, @@ -565,7 +565,7 @@ pub(crate) async fn connect_to_peer( connector .connect_peer(node_id_str, node.addresses[0].clone().addr) .await - .map_err(OfferError::PeerConnectError)?; + .map_err(|e| OfferError::PeerConnectError(e.message().to_string()))?; Ok(()) } @@ -596,7 +596,7 @@ pub(super) async fn get_node_id_from_scid( .lightning_read_only() .get_chan_info(get_info_request) .await - .map_err(OfferError::GetChannelInfo)? + .map_err(|e| OfferError::GetChannelInfo(e.message().to_string()))? .into_inner(); match direction { Direction::NodeOne => Ok(channel_info.node1_pub), diff --git a/src/offers/mod.rs b/src/offers/mod.rs index f570ad0..81608d7 100644 --- a/src/offers/mod.rs +++ b/src/offers/mod.rs @@ -6,7 +6,6 @@ use lightning::{ offers::{merkle::SignError, parse::Bolt12ParseError, parse::Bolt12SemanticError}, }; use tonic::{Code, Status}; -use tonic_lnd::tonic::Status as LndStatus; use tonic_types::{ErrorDetails, StatusExt}; mod client_impls; @@ -29,23 +28,23 @@ pub enum OfferError { /// SignError indicates a failure to sign the invoice request. SignError(SignError), /// DeriveKeyFailure indicates a failure to derive key for signing the invoice request. - DeriveKeyFailure(LndStatus), + DeriveKeyFailure(String), /// User provided an invalid amount. InvalidAmount(String), /// Invalid currency contained in the offer. InvalidCurrency, /// Unable to connect to peer. - PeerConnectError(LndStatus), + PeerConnectError(String), /// No node address. NodeAddressNotFound, /// Cannot list peers. - ListPeersFailure(LndStatus), + ListPeersFailure(String), /// Failure to build a reply path. BuildBlindedPathFailure, /// Unable to find or send to payment route. - RouteFailure(LndStatus), + RouteFailure(String), /// Failed to track payment. - TrackFailure(LndStatus), + TrackFailure(String), /// Failed to send payment. PaymentFailure, /// Failed to receive an invoice back from offer creator before the timeout. @@ -53,15 +52,15 @@ pub enum OfferError { /// Failed to find introduction node for blinded path. IntroductionNodeNotFound, /// Cannot fetch channel info. - GetChannelInfo(LndStatus), + GetChannelInfo(String), /// Failed to create offer. CreateOfferFailure(Bolt12SemanticError), /// Failed to create offer with expiry time given system clock. CreateOfferTimeFailure, /// Failed to add invoice. - AddInvoiceFailure(LndStatus), + AddInvoiceFailure(String), /// Failed to decode payment request. - DecodePaymentRequestFailure(LndStatus), + DecodePaymentRequestFailure(String), /// Failed to parse payment hash. ParsePaymentHashFailure(String), /// Failed to parse offer. @@ -71,7 +70,7 @@ pub enum OfferError { /// Failed to encode invoice. EncodeInvoiceFailure(BitcoinIoError), /// Cannot list channels. - ListChannelsFailure(LndStatus), + ListChannelsFailure(String), } impl OfferError { @@ -135,28 +134,28 @@ impl Display for OfferError { } OfferError::BuildUIRFailure(e) => write!(f, "Failed to build invoice request: {e:?}"), OfferError::SignError(e) => write!(f, "Failed to sign invoice request: {e:?}"), - OfferError::DeriveKeyFailure(e) => write!(f, "Failed to derive key: {e:?}"), - OfferError::InvalidAmount(e) => write!(f, "Invalid amount: {e:?}"), + OfferError::DeriveKeyFailure(e) => write!(f, "Failed to derive key: {e}"), + OfferError::InvalidAmount(e) => write!(f, "Invalid amount: {e}"), OfferError::InvalidCurrency => write!(f, "Only bitcoin currency is supported"), - OfferError::PeerConnectError(e) => write!(f, "Failed to connect to peer: {e:?}"), + OfferError::PeerConnectError(e) => write!(f, "Failed to connect to peer: {e}"), OfferError::NodeAddressNotFound => write!(f, "Node address not found"), - OfferError::ListPeersFailure(e) => write!(f, "Failed to list peers: {e:?}"), + OfferError::ListPeersFailure(e) => write!(f, "Failed to list peers: {e}"), OfferError::BuildBlindedPathFailure => write!(f, "Failed to build blinded path"), - OfferError::RouteFailure(e) => write!(f, "Failed to route payment: {e:?}"), - OfferError::TrackFailure(e) => write!(f, "Failed to track payment: {e:?}"), + OfferError::RouteFailure(e) => write!(f, "Failed to route payment: {e}"), + OfferError::TrackFailure(e) => write!(f, "Failed to track payment: {e}"), OfferError::PaymentFailure => write!(f, "Payment failed"), OfferError::InvoiceTimeout(e) => { - write!(f, "Invoice request timed out after {e:?} seconds") + write!(f, "Invoice request timed out after {e} seconds") } OfferError::IntroductionNodeNotFound => write!(f, "Introduction node not found"), - OfferError::GetChannelInfo(e) => write!(f, "Failed to get channel info: {e:?}"), + OfferError::GetChannelInfo(e) => write!(f, "Failed to get channel info: {e}"), OfferError::CreateOfferFailure(e) => write!(f, "Failed to create offer: {e:?}"), OfferError::CreateOfferTimeFailure => write!(f, "Invalid offer expiry time"), OfferError::AddInvoiceFailure(e) => { - write!(f, "Failed to add invoice to lnd node: {e:?}") + write!(f, "Failed to add invoice to lnd node: {e}") } OfferError::DecodePaymentRequestFailure(e) => { - write!(f, "Failed to decode payment request: {e:?}") + write!(f, "Failed to decode payment request: {e}") } OfferError::ParsePaymentHashFailure(e) => { write!(f, "Failed to parse payment hash: {e:?}") diff --git a/src/server.rs b/src/server.rs index e06f451..1404748 100644 --- a/src/server.rs +++ b/src/server.rs @@ -80,7 +80,7 @@ impl Offers for LNDKServer { .lightning() .get_info(GetInfoRequest {}) .await - .map_err(LndError::ServiceUnavailable)? + .map_err(|e| LndError::ServiceUnavailable(e.message().to_string()))? .into_inner(); let network = get_network(info).await?; @@ -149,7 +149,7 @@ impl Offers for LNDKServer { .lightning() .get_info(GetInfoRequest {}) .await - .map_err(LndError::ServiceUnavailable)? + .map_err(|e| LndError::ServiceUnavailable(e.message().to_string()))? .into_inner(); let network = get_network(info).await?; @@ -239,7 +239,7 @@ impl Offers for LNDKServer { .lightning() .get_info(GetInfoRequest {}) .await - .map_err(LndError::ServiceUnavailable)? + .map_err(|e| LndError::ServiceUnavailable(e.message().to_string()))? .into_inner(); let network = get_network(info).await?; let quantity = parse_quantity(inner_request.quantity);