chore: standardize error responses, adding stable machine codes

Add stable, machine-readable error codes responses alongside
human-readable messages for better error handling and processing.
This commit is contained in:
Ignacio Porte 2025-08-28 22:59:06 -04:00
parent 24f3f0ff5a
commit e44d422108
2 changed files with 165 additions and 104 deletions

View file

@ -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<OfferError> for Status {
fn from(error: OfferError) -> Self {
error.to_status()
}
}

View file

@ -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<OfferHandler>,
#[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<u64>) -> Result<Option<Quantity>, ()> {
fn parse_quantity(rpc_quantity: Option<u64>) -> Result<Option<Quantity>, OfferError> {
let quantity = match rpc_quantity {
Some(quantity) => quantity,
None => return Ok(None),
@ -351,7 +295,9 @@ fn parse_quantity(rpc_quantity: Option<u64>) -> Result<Option<Quantity>, ()> {
}
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<String, Status> {
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<String, Status> {
fn encode_invoice_as_hex(invoice: &Bolt12Invoice) -> Result<String, OfferError> {
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))
}