mirror of
https://github.com/lndk-org/lndk.git
synced 2026-08-13 12:33:05 +02:00
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:
parent
24f3f0ff5a
commit
e44d422108
2 changed files with 165 additions and 104 deletions
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
172
src/server.rs
172
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<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))
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue