diff --git a/src/lib.rs b/src/lib.rs index 61dc862..4df5071 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -153,7 +153,9 @@ impl LndkOnionMessenger { .await .expect("failed to get info") .into_inner(); - let network = get_network(info.clone()).await?; + let network = get_network(info.clone()) + .await + .expect("failed to get network"); let pubkey = PublicKey::from_str(&info.identity_pubkey).unwrap(); info!("Starting lndk on {network} network for node: {pubkey}."); diff --git a/src/lnd.rs b/src/lnd.rs index b90e5fd..1f8404d 100644 --- a/src/lnd.rs +++ b/src/lnd.rs @@ -24,6 +24,7 @@ use std::error::Error; use std::fmt::Display; use std::path::PathBuf; use std::{fmt, fs}; +use tonic::{Code, Status}; use tonic_lnd::lnrpc::AddInvoiceResponse; use tonic_lnd::lnrpc::PayReq; use tonic_lnd::lnrpc::{ @@ -31,7 +32,7 @@ use tonic_lnd::lnrpc::{ QueryRoutesResponse, Route, }; use tonic_lnd::signrpc::KeyLocator; -use tonic_lnd::tonic::Status; +use tonic_lnd::tonic::Status as LndStatus; use tonic_lnd::verrpc::Version; use tonic_lnd::{Client, Error as ConnectError}; @@ -52,11 +53,13 @@ const SEED_KEY_INDEX: i32 = 425; /// get_lnd_client connects to LND's grpc api using the config provided, blocking until a connection /// is established. -pub fn get_lnd_client(cfg: LndCfg) -> Result { +pub fn get_lnd_client(cfg: LndCfg) -> Result { match cfg.creds { - Creds::Path { macaroon, cert } => block_on(tonic_lnd::connect(cfg.address, cert, macaroon)), + Creds::Path { macaroon, cert } => block_on(tonic_lnd::connect(cfg.address, cert, macaroon)) + .map_err(LndError::ConnectError), Creds::String { macaroon, cert } => { block_on(tonic_lnd::connect_from_memory(cfg.address, cert, macaroon)) + .map_err(LndError::ConnectError) } } } @@ -359,8 +362,74 @@ impl fmt::Display for NetworkParseError { } } +#[derive(Debug)] +/// LndError represents errors that occur when interacting with LND. +pub enum LndError { + /// Failed to connect to LND node. + ConnectError(ConnectError), + /// Failed to parse network configuration. + NetworkParseError(NetworkParseError), + /// LND node is not connected to bitcoin network. + NetworkNotConnected, + /// LND service is unavailable or not responding. + ServiceUnavailable(LndStatus), +} + +impl LndError { + pub fn code(&self) -> &'static str { + match self { + LndError::ConnectError(_) => "CONNECT_ERROR", + LndError::NetworkParseError(_) => "NETWORK_PARSE_ERROR", + LndError::NetworkNotConnected => "NETWORK_NOT_CONNECTED", + LndError::ServiceUnavailable(_) => "SERVICE_UNAVAILABLE", + } + } + + pub fn grpc_code(&self) -> Code { + match self { + LndError::NetworkParseError(_) => Code::InvalidArgument, + LndError::ConnectError(_) + | LndError::NetworkNotConnected + | LndError::ServiceUnavailable(_) => Code::Unavailable, + } + } + + pub fn to_status(self) -> Status { + let error_code = self.code(); + let grpc_code = self.grpc_code(); + let human_message = self.to_string(); + + let error_info = format!(r#"{{"reason": "{}", "domain": "lndk"}}"#, error_code); + + Status::with_details(grpc_code, human_message, error_info.into()) + } +} + +impl Display for LndError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + LndError::ConnectError(e) => write!(f, "Failed to connect to LND: {e}"), + LndError::NetworkParseError(e) => { + write!(f, "Failed to parse network configuration: {e}") + } + LndError::NetworkNotConnected => { + write!(f, "LND node is not connected to bitcoin network") + } + LndError::ServiceUnavailable(e) => write!(f, "LND service is unavailable: {e}"), + } + } +} + +impl Error for LndError {} + +impl From for Status { + fn from(error: LndError) -> Self { + error.to_status() + } +} + // get_network grabs what network lnd is running on from the LND API. -pub async fn get_network(info: GetInfoResponse) -> Result { +pub async fn get_network(info: GetInfoResponse) -> Result { let mut network_str = None; #[allow(deprecated)] for chain in info.chains { @@ -368,11 +437,12 @@ pub async fn get_network(info: GetInfoResponse) -> Result { network_str = Some(chain.network.clone()) } } - if network_str.is_none() { + let network_str = network_str.ok_or_else(|| { error!("lnd node is not connected to bitcoin network as expected"); - return Err(()); - } - Ok(string_to_network(&network_str.unwrap()).unwrap()) + LndError::NetworkNotConnected + })?; + + string_to_network(&network_str).map_err(LndError::NetworkParseError) } pub fn string_to_network(network_str: &str) -> Result { @@ -483,13 +553,13 @@ pub trait MessageSigner { /// PeerConnector provides a layer of abstraction over the LND API for connecting to a peer. #[async_trait] pub trait PeerConnector { - async fn list_peers(&mut self) -> Result; - async fn connect_peer(&mut self, node_id: String, addr: String) -> Result<(), Status>; + async fn list_peers(&mut self) -> Result; + async fn connect_peer(&mut self, node_id: String, addr: String) -> Result<(), LndStatus>; async fn get_node_info( &mut self, pub_key: String, include_channels: bool, - ) -> Result; + ) -> Result; } /// InvoicePayer provides a layer of abstraction over the LND API for paying for a BOLT 12 invoice. @@ -503,18 +573,18 @@ pub trait InvoicePayer { fee_ppm: u32, msats: u64, fee_limit: Option, - ) -> Result; + ) -> Result; async fn send_to_route( &mut self, payment_hash: [u8; 32], route: Route, - ) -> Result; + ) -> Result; async fn track_payment(&mut self, payment_hash: [u8; 32]) -> Result; } #[async_trait] pub trait OfferCreator { - async fn get_info(&mut self) -> Result; + async fn get_info(&mut self) -> Result; } #[async_trait] @@ -522,9 +592,12 @@ pub trait Bolt12InvoiceCreator { async fn add_invoice( &mut self, invoice_request: InvoiceRequest, - ) -> Result; + ) -> Result; - async fn decode_payment_request(&mut self, payment_request: String) -> Result; + async fn decode_payment_request( + &mut self, + payment_request: String, + ) -> Result; } #[cfg(test)] diff --git a/src/offers/mod.rs b/src/offers/mod.rs index 600f080..d240d42 100644 --- a/src/offers/mod.rs +++ b/src/offers/mod.rs @@ -6,7 +6,7 @@ use lightning::{ offers::{merkle::SignError, parse::Bolt12ParseError, parse::Bolt12SemanticError}, }; use tonic::{Code, Status}; -use tonic_lnd::tonic::Status as TonicStatus; +use tonic_lnd::tonic::Status as LndStatus; mod client_impls; pub mod handler; @@ -27,23 +27,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(TonicStatus), + DeriveKeyFailure(LndStatus), /// User provided an invalid amount. InvalidAmount(String), /// Invalid currency contained in the offer. InvalidCurrency, /// Unable to connect to peer. - PeerConnectError(TonicStatus), + PeerConnectError(LndStatus), /// No node address. NodeAddressNotFound, /// Cannot list peers. - ListPeersFailure(TonicStatus), + ListPeersFailure(LndStatus), /// Failure to build a reply path. BuildBlindedPathFailure, /// Unable to find or send to payment route. - RouteFailure(TonicStatus), + RouteFailure(LndStatus), /// Failed to track payment. - TrackFailure(TonicStatus), + TrackFailure(LndStatus), /// Failed to send payment. PaymentFailure, /// Failed to receive an invoice back from offer creator before the timeout. @@ -51,15 +51,15 @@ pub enum OfferError { /// Failed to find introduction node for blinded path. IntroductionNodeNotFound, /// Cannot fetch channel info. - GetChannelInfo(TonicStatus), + GetChannelInfo(LndStatus), /// Failed to create offer. CreateOfferFailure(Bolt12SemanticError), /// Failed to create offer with expiry time given system clock. CreateOfferTimeFailure, /// Failed to add invoice. - AddInvoiceFailure(TonicStatus), + AddInvoiceFailure(LndStatus), /// Failed to decode payment request. - DecodePaymentRequestFailure(TonicStatus), + DecodePaymentRequestFailure(LndStatus), /// Failed to parse payment hash. ParsePaymentHashFailure(String), /// Failed to parse offer. diff --git a/src/server.rs b/src/server.rs index a6baee9..df6ab41 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,4 +1,4 @@ -use crate::lnd::{get_lnd_client, get_network, Creds, LndCfg}; +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}; @@ -68,8 +68,7 @@ impl Offers for LNDKServer { macaroon, }; let lnd_cfg = LndCfg::new(self.address.clone(), creds); - let mut client = get_lnd_client(lnd_cfg) - .map_err(|e| Status::unavailable(format!("Couldn't connect to lnd: {e}")))?; + 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)?; @@ -80,11 +79,9 @@ impl Offers for LNDKServer { .lightning() .get_info(GetInfoRequest {}) .await - .expect("failed to get info") + .map_err(LndError::ServiceUnavailable)? .into_inner(); - let network = get_network(info) - .await - .map_err(|e| Status::internal(format!("{e:?}")))?; + let network = get_network(info).await?; let fee_limit = create_fee_limit(inner_request.fee_limit, inner_request.fee_limit_percent); @@ -140,8 +137,7 @@ impl Offers for LNDKServer { macaroon, }; let lnd_cfg = LndCfg::new(self.address.clone(), creds); - let mut client = get_lnd_client(lnd_cfg) - .map_err(|e| Status::unavailable(format!("Couldn't connect to lnd: {e}")))?; + 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)?; @@ -153,11 +149,9 @@ impl Offers for LNDKServer { .lightning() .get_info(GetInfoRequest {}) .await - .expect("failed to get info") + .map_err(LndError::ServiceUnavailable)? .into_inner(); - let network = get_network(info) - .await - .map_err(|e| Status::internal(format!("{e:?}")))?; + let network = get_network(info).await?; let cfg = PayOfferParams { offer, @@ -202,8 +196,7 @@ impl Offers for LNDKServer { macaroon, }; let lnd_cfg = LndCfg::new(self.address.clone(), creds); - let client = get_lnd_client(lnd_cfg) - .map_err(|e| Status::unavailable(format!("Couldn't connect to lnd: {e}")))?; + let client = get_lnd_client(lnd_cfg)?; let inner_request = request.get_ref(); let invoice_string: Bolt12InvoiceString = inner_request.invoice.clone().into(); @@ -241,18 +234,15 @@ impl Offers for LNDKServer { macaroon, }; let lnd_cfg = LndCfg::new(self.address.clone(), creds); - let mut client = get_lnd_client(lnd_cfg) - .map_err(|e| Status::unavailable(format!("Couldn't connect to lnd: {e}")))?; + let mut client = get_lnd_client(lnd_cfg)?; let inner_request = request.get_ref(); let info = client .lightning() .get_info(GetInfoRequest {}) .await - .expect("failed to get info") + .map_err(LndError::ServiceUnavailable)? .into_inner(); - let network = get_network(info) - .await - .map_err(|e| Status::internal(format!("{e:?}")))?; + let network = get_network(info).await?; let quantity = parse_quantity(inner_request.quantity)?; let request = CreateOfferParams {