diff --git a/src/offers/mod.rs b/src/offers/mod.rs index dd2dda7..600f080 100644 --- a/src/offers/mod.rs +++ b/src/offers/mod.rs @@ -1,10 +1,12 @@ use std::{error::Error, fmt::Display}; +use bitcoin::io::Error as BitcoinIoError; use lightning::{ - ln::channelmanager::PaymentId, - offers::{merkle::SignError, parse::Bolt12SemanticError}, + ln::{channelmanager::PaymentId, msgs::DecodeError}, + offers::{merkle::SignError, parse::Bolt12ParseError, parse::Bolt12SemanticError}, }; -use tonic_lnd::tonic::Status; +use tonic::{Code, Status}; +use tonic_lnd::tonic::Status as TonicStatus; mod client_impls; pub mod handler; @@ -25,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(Status), + DeriveKeyFailure(TonicStatus), /// User provided an invalid amount. InvalidAmount(String), /// Invalid currency contained in the offer. InvalidCurrency, /// Unable to connect to peer. - PeerConnectError(Status), + PeerConnectError(TonicStatus), /// No node address. NodeAddressNotFound, /// Cannot list peers. - ListPeersFailure(Status), + ListPeersFailure(TonicStatus), /// Failure to build a reply path. BuildBlindedPathFailure, /// Unable to find or send to payment route. - RouteFailure(Status), + RouteFailure(TonicStatus), /// Failed to track payment. - TrackFailure(Status), + TrackFailure(TonicStatus), /// Failed to send payment. PaymentFailure, /// Failed to receive an invoice back from offer creator before the timeout. @@ -49,17 +51,75 @@ pub enum OfferError { /// Failed to find introduction node for blinded path. IntroductionNodeNotFound, /// Cannot fetch channel info. - GetChannelInfo(Status), + GetChannelInfo(TonicStatus), /// Failed to create offer. CreateOfferFailure(Bolt12SemanticError), /// Failed to create offer with expiry time given system clock. CreateOfferTimeFailure, /// Failed to add invoice. - AddInvoiceFailure(Status), + AddInvoiceFailure(TonicStatus), /// Failed to decode payment request. - DecodePaymentRequestFailure(Status), + DecodePaymentRequestFailure(TonicStatus), /// Failed to parse payment hash. ParsePaymentHashFailure(String), + /// Failed to parse offer. + ParseOfferFailure(Bolt12ParseError), + /// Failed to parse invoice. + ParseInvoiceFailure(DecodeError), + /// Failed to encode invoice. + EncodeInvoiceFailure(BitcoinIoError), +} + +impl OfferError { + pub fn code(&self) -> &'static str { + match self { + OfferError::CreateOfferFailure(_) => "CREATE_OFFER_FAILURE", + OfferError::CreateOfferTimeFailure => "CREATE_OFFER_TIME_FAILURE", + OfferError::AddInvoiceFailure(_) => "ADD_INVOICE_FAILURE", + OfferError::DecodePaymentRequestFailure(_) => "DECODE_PAYMENT_REQUEST_FAILURE", + OfferError::ParsePaymentHashFailure(_) => "PARSE_PAYMENT_HASH_FAILURE", + OfferError::ParseOfferFailure(_) => "PARSE_OFFER_FAILURE", + OfferError::ParseInvoiceFailure(_) => "PARSE_INVOICE_FAILURE", + OfferError::EncodeInvoiceFailure(_) => "ENCODE_INVOICE_FAILURE", + OfferError::InvalidAmount(_) => "INVALID_AMOUNT", + OfferError::InvalidCurrency => "INVALID_CURRENCY", + OfferError::AlreadyProcessing(_) => "ALREADY_PROCESSING", + OfferError::BuildUIRFailure(_) => "BUILD_UIR_FAILURE", + OfferError::SignError(_) => "SIGN_ERROR", + OfferError::DeriveKeyFailure(_) => "DERIVE_KEY_FAILURE", + OfferError::PeerConnectError(_) => "PEER_CONNECT_ERROR", + OfferError::NodeAddressNotFound => "NODE_ADDRESS_NOT_FOUND", + OfferError::ListPeersFailure(_) => "LIST_PEERS_FAILURE", + OfferError::BuildBlindedPathFailure => "BUILD_BLINDED_PATH_FAILURE", + OfferError::RouteFailure(_) => "ROUTE_FAILURE", + OfferError::TrackFailure(_) => "TRACK_FAILURE", + OfferError::PaymentFailure => "PAYMENT_FAILURE", + OfferError::InvoiceTimeout(_) => "INVOICE_TIMEOUT", + OfferError::IntroductionNodeNotFound => "INTRODUCTION_NODE_NOT_FOUND", + OfferError::GetChannelInfo(_) => "GET_CHANNEL_INFO", + } + } + + pub fn grpc_code(&self) -> Code { + match self { + OfferError::InvalidAmount(_) + | OfferError::InvalidCurrency + | OfferError::ParseOfferFailure(_) + | OfferError::ParseInvoiceFailure(_) + | OfferError::EncodeInvoiceFailure(_) => Code::InvalidArgument, + _ => Code::Internal, + } + } + + 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 OfferError { @@ -103,8 +163,23 @@ impl Display for OfferError { OfferError::ParsePaymentHashFailure(e) => { write!(f, "Could not 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:?}") + } + 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:?}") + } } } } impl Error for OfferError {} + +impl From for Status { + fn from(error: OfferError) -> Self { + error.to_status() + } +} diff --git a/src/server.rs b/src/server.rs index 9d0c8a2..b6ac9d3 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,8 +1,9 @@ use crate::lnd::{get_lnd_client, get_network, Creds, LndCfg}; use crate::lndkrpc::{CreateOfferRequest, CreateOfferResponse}; +use crate::offers::get_destination; use crate::offers::handler::{CreateOfferParams, PayOfferParams}; use crate::offers::validate_amount; -use crate::offers::{get_destination, OfferError}; +use crate::offers::OfferError; use crate::{lndkrpc, Bolt12InvoiceString, OfferHandler, TLS_CERT_FILENAME, TLS_KEY_FILENAME}; use bitcoin::secp256k1::PublicKey; use lightning::blinded_path::payment::BlindedPaymentPath; @@ -10,6 +11,7 @@ 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::parse::{Bolt12ParseError, Bolt12SemanticError}; use lightning::sign::EntropySource; use lightning::util::ser::Writeable; use lndkrpc::offers_server::Offers; @@ -33,6 +35,7 @@ use tonic::metadata::MetadataMap; use tonic::transport::Identity; use tonic::{Request, Response, Status}; use tonic_lnd::lnrpc::GetInfoRequest; + pub struct LNDKServer { offer_handler: Arc, #[allow(dead_code)] @@ -77,18 +80,9 @@ impl Offers for LNDKServer { .map_err(|e| Status::unavailable(format!("Couldn't connect to lnd: {e}")))?; let inner_request = request.get_ref(); - let offer = Offer::from_str(&inner_request.offer).map_err(|e| { - Status::invalid_argument(format!( - "The provided offer was invalid. Please provide a valid offer in bech32 format, - i.e. starting with 'lno'. Error: {e:?}" - )) - })?; + let offer = Offer::from_str(&inner_request.offer).map_err(OfferError::ParseOfferFailure)?; - let destination = get_destination(&offer).await.map_err(|e| { - Status::internal(format!( - "Internal error: Couldn't get destination from offer: {e:?}" - )) - })?; + let destination = get_destination(&offer).await?; let reply_path = None; let info = client .lightning() @@ -114,24 +108,11 @@ impl Offers for LNDKServer { fee_limit, }; - let payment = match self.offer_handler.pay_offer(cfg).await { - Ok(payment) => { - log::info!( - "Payment succeeded with preimage {}.", - payment.payment_preimage - ); - payment - } - Err(e) => match e { - OfferError::InvalidAmount(e) => { - return Err(Status::invalid_argument(e.to_string())) - } - OfferError::InvalidCurrency => { - return Err(Status::invalid_argument(format!("{e}"))) - } - _ => return Err(Status::internal(format!("Internal error: {e}"))), - }, - }; + let payment = self.offer_handler.pay_offer(cfg).await?; + log::info!( + "Payment succeeded with preimage: {}", + payment.payment_preimage + ); let reply = PayOfferResponse { payment_preimage: payment.payment_preimage, @@ -147,8 +128,8 @@ impl Offers for LNDKServer { log::info!("Received a request: {:?}", request.get_ref()); let invoice_string: Bolt12InvoiceString = request.get_ref().invoice.clone().into(); - let invoice = Bolt12Invoice::try_from(invoice_string) - .map_err(|e| Status::invalid_argument(e.to_string()))?; + let invoice = + Bolt12Invoice::try_from(invoice_string).map_err(OfferError::ParseInvoiceFailure)?; let reply: Bolt12InvoiceContents = generate_bolt12_invoice_contents(&invoice); Ok(Response::new(reply)) @@ -171,16 +152,9 @@ impl Offers for LNDKServer { .map_err(|e| Status::unavailable(format!("Couldn't connect to lnd: {e}")))?; let inner_request = request.get_ref(); - let offer = Offer::from_str(&inner_request.offer).map_err(|e| { - Status::invalid_argument(format!( - "The provided offer was invalid. Please provide a valid offer in bech32 format, - i.e. starting with 'lno'. Error: {e:?}" - )) - })?; + let offer = Offer::from_str(&inner_request.offer).map_err(OfferError::ParseOfferFailure)?; - let destination = get_destination(&offer) - .await - .map_err(|e| Status::unavailable(format!("Couldn't find destination: {e}")))?; + let destination = get_destination(&offer).await?; let reply_path = None; let info = client @@ -205,21 +179,8 @@ impl Offers for LNDKServer { fee_limit: None, }; - let (invoice, _, payment_id) = match self.offer_handler.get_invoice(cfg).await { - Ok(invoice) => { - log::info!("Invoice request succeeded for payment_id {}.", invoice.2); - invoice - } - Err(e) => match e { - OfferError::InvalidAmount(e) => { - return Err(Status::invalid_argument(e.to_string())) - } - OfferError::InvalidCurrency => { - return Err(Status::invalid_argument(format!("{e}"))) - } - _ => return Err(Status::internal(format!("Internal error: {e}"))), - }, - }; + let (invoice, _, payment_id) = self.offer_handler.get_invoice(cfg).await?; + log::info!("Invoice request succeeded."); // We need to remove the payment from our tracking map now. // TODO: This is a hack to remove the payment from the tracking map. We should do it when @@ -254,32 +215,19 @@ impl Offers for LNDKServer { let inner_request = request.get_ref(); let invoice_string: Bolt12InvoiceString = inner_request.invoice.clone().into(); - let invoice = Bolt12Invoice::try_from(invoice_string).map_err(|e| { - Status::invalid_argument(format!( - "The provided invoice was invalid. Please provide a valid invoice in hex format. - Error: {e:?}" - )) - })?; + let invoice = + Bolt12Invoice::try_from(invoice_string).map_err(OfferError::ParseInvoiceFailure)?; - let amount = match validate_amount(invoice.amount().as_ref(), inner_request.amount).await { - Ok(amount) => amount, - Err(e) => return Err(Status::invalid_argument(e.to_string())), - }; + let amount = validate_amount(invoice.amount().as_ref(), inner_request.amount).await?; let payment_id = PaymentId(self.offer_handler.messenger_utils.get_secure_random_bytes()); let fee_limit = create_fee_limit(inner_request.fee_limit, inner_request.fee_limit_percent); - let invoice = match self + let invoice = self .offer_handler .pay_invoice(client, amount, &invoice, payment_id, fee_limit) - .await - { - Ok(invoice) => { - log::info!("Invoice paid."); - invoice - } - Err(e) => return Err(Status::internal(format!("Error paying invoice: {e}"))), - }; + .await?; + log::info!("Invoice paid."); let reply = PayInvoiceResponse { payment_preimage: invoice.payment_preimage, @@ -313,8 +261,7 @@ impl Offers for LNDKServer { let network = get_network(info) .await .map_err(|e| Status::internal(format!("{e:?}")))?; - let quantity = parse_quantity(inner_request.quantity) - .map_err(|_| Status::invalid_argument("Invalid quantity provided"))?; + let quantity = parse_quantity(inner_request.quantity)?; let request = CreateOfferParams { client, @@ -325,10 +272,7 @@ impl Offers for LNDKServer { quantity, expiry: inner_request.expiry.map(Duration::from_secs), }; - let offer = match self.offer_handler.create_offer(request).await { - Ok(offer) => offer, - Err(e) => return Err(Status::internal(format!("Error creating offer: {e}"))), - }; + let offer = self.offer_handler.create_offer(request).await?; let reply = CreateOfferResponse { offer: offer.to_string(), @@ -337,7 +281,7 @@ impl Offers for LNDKServer { } } -fn parse_quantity(rpc_quantity: Option) -> Result, ()> { +fn parse_quantity(rpc_quantity: Option) -> Result, OfferError> { let quantity = match rpc_quantity { Some(quantity) => quantity, None => return Ok(None), @@ -351,7 +295,9 @@ fn parse_quantity(rpc_quantity: Option) -> Result, ()> { } let amount = NonZeroU64::new(quantity); if amount.is_none() { - return Err(()); + return Err(OfferError::ParseOfferFailure( + Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidQuantity), + )); } Ok(Some(Quantity::Bounded(amount.unwrap()))) } @@ -362,20 +308,60 @@ fn check_auth_metadata(metadata: &MetadataMap) -> Result { let macaroon = match metadata.get("macaroon") { Some(macaroon_hex) => macaroon_hex .to_str() - .map_err(|e| { - Status::invalid_argument(format!("Invalid macaroon string provided: {e}")) - })? + .map_err(|e| AuthError::InvalidMacaroon(e.to_string()).to_status())? .to_string(), - _ => { - return Err(Status::unauthenticated( - "No LND macaroon provided: Make sure to provide macaroon in request metadata", - )) - } + _ => return Err(AuthError::MissingMacaroon.to_status()), }; Ok(macaroon) } +#[derive(Debug)] +pub enum AuthError { + InvalidMacaroon(String), + MissingMacaroon, +} + +impl std::fmt::Display for AuthError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AuthError::InvalidMacaroon(e) => write!(f, "Invalid macaroon string provided: {e}"), + AuthError::MissingMacaroon => write!( + f, + "No LND macaroon provided: Make sure to provide macaroon in request metadata" + ), + } + } +} + +impl std::error::Error for AuthError {} + +impl AuthError { + pub fn code(&self) -> &'static str { + match self { + AuthError::InvalidMacaroon(_) => "INVALID_MACAROON", + AuthError::MissingMacaroon => "MISSING_MACAROON", + } + } + + pub fn grpc_code(&self) -> tonic::Code { + match self { + AuthError::InvalidMacaroon(_) => tonic::Code::InvalidArgument, + AuthError::MissingMacaroon => tonic::Code::Unauthenticated, + } + } + + 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()) + } +} + /// An error that occurs when generating TLS credentials. #[derive(Debug)] pub enum CertificateGenFailure { @@ -492,11 +478,11 @@ fn generate_bolt12_invoice_contents(invoice: &Bolt12Invoice) -> lndkrpc::Bolt12I } } -fn encode_invoice_as_hex(invoice: &Bolt12Invoice) -> Result { +fn encode_invoice_as_hex(invoice: &Bolt12Invoice) -> Result { let mut buffer = Vec::new(); invoice .write(&mut buffer) - .map_err(|e| Status::internal(format!("Error serializing invoice: {e}")))?; + .map_err(OfferError::EncodeInvoiceFailure)?; Ok(hex::encode(buffer)) }