Skip to content

Use LdkServerError for error handling in all APIs. #46

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Feb 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2fb0278
Derive traits for LdkServerError.
G8XSU Feb 26, 2025
87e6c01
Implement Display & std::Error for LdkServerError.
G8XSU Feb 26, 2025
5389155
Add constructor for LdkServerError.
G8XSU Feb 26, 2025
0e9181b
Return LdkServerError from NodeService.
G8XSU Feb 26, 2025
221116f
Return LdkServerError from GetNodeInfo API.
G8XSU Feb 26, 2025
88095eb
Return LdkServerError from GetBalances API.
G8XSU Feb 26, 2025
8dd2ae0
Impl from<NodeError> for LdkServerError.
G8XSU Feb 26, 2025
3b8c96b
Return LdkServerError from OnchainReceive API.
G8XSU Feb 26, 2025
d43f9f0
Return LdkServerError from OnchainSend API.
G8XSU Feb 26, 2025
5c08b42
Return LdkServerError from Bolt11Receive API.
G8XSU Feb 26, 2025
bc0cb45
Return LdkServerError from Bolt11Send API.
G8XSU Feb 26, 2025
8aa6bde
Return LdkServerError from Bolt12Receive API.
G8XSU Feb 26, 2025
403941b
Return LdkServerError from Bolt12Send API.
G8XSU Feb 26, 2025
d6cc6d9
Return LdkServerError from OpenChannel API.
G8XSU Feb 26, 2025
029a28d
Return LdkServerError from CloseChannel API.
G8XSU Feb 26, 2025
cd91495
Return LdkServerError from ListChannels API.
G8XSU Feb 26, 2025
f8015cd
Return LdkServerError from UpdateChannelConfig API.
G8XSU Feb 26, 2025
6e25671
Return LdkServerError from GetPaymentDetails API.
G8XSU Feb 26, 2025
ba5bf69
Return LdkServerError from ListPayments API.
G8XSU Feb 26, 2025
9b4adee
Return LdkServerError from ListForwardedPayments API.
G8XSU Feb 26, 2025
b4f20f5
Initialize vec with capacity, to pre-allocate space.
G8XSU Feb 26, 2025
db54e65
Return protobuf serialized ErrorResponse from APIs.
G8XSU Feb 27, 2025
605b23f
Add ErrorResponse handling in client.
G8XSU Feb 27, 2025
f795976
Add ErrorResponse handling in cli.
G8XSU Feb 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions ldk-server-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use clap::{Parser, Subcommand};
use ldk_server_client::client::LdkServerClient;
use ldk_server_client::error::LdkServerError;
use ldk_server_client::error::LdkServerErrorCode::{
AuthError, InternalError, InternalServerError, InvalidRequestError, LightningError,
};
use ldk_server_client::ldk_server_protos::api::{
Bolt11ReceiveRequest, Bolt11SendRequest, Bolt12ReceiveRequest, Bolt12SendRequest,
GetBalancesRequest, GetNodeInfoRequest, ListChannelsRequest, ListPaymentsRequest,
Expand Down Expand Up @@ -123,7 +126,8 @@ async fn main() {
kind: Some(bolt11_invoice_description::Kind::Hash(hash)),
}),
(Some(_), Some(_)) => {
handle_error(LdkServerError::InternalError(
handle_error(LdkServerError::new(
InternalError,
"Only one of description or description_hash can be set.".to_string(),
));
},
Expand Down Expand Up @@ -191,15 +195,22 @@ async fn main() {
fn handle_response_result<Rs: ::prost::Message>(response: Result<Rs, LdkServerError>) {
match response {
Ok(response) => {
println!("{:?}", response);
println!("Success: {:?}", response);
},
Err(e) => {
handle_error(e);
},
};
}
}

fn handle_error(e: LdkServerError) -> ! {
eprintln!("Error executing command: {:?}", e);
let error_type = match e.error_code {
InvalidRequestError => "Invalid Request",
AuthError => "Authentication Error",
LightningError => "Lightning Error",
InternalServerError => "Internal Server Error",
InternalError => "Internal Error",
};
eprintln!("Error ({}): {}", error_type, e.message);
std::process::exit(1); // Exit with status code 1 on error.
}
45 changes: 34 additions & 11 deletions ldk-server-client/src/client.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use prost::Message;

use crate::error::LdkServerError;
use crate::error::LdkServerErrorCode::{
AuthError, InternalError, InternalServerError, InvalidRequestError, LightningError,
};
use ldk_server_protos::api::{
Bolt11ReceiveRequest, Bolt11ReceiveResponse, Bolt11SendRequest, Bolt11SendResponse,
Bolt12ReceiveRequest, Bolt12ReceiveResponse, Bolt12SendRequest, Bolt12SendResponse,
Expand All @@ -9,6 +12,7 @@ use ldk_server_protos::api::{
ListPaymentsRequest, ListPaymentsResponse, OnchainReceiveRequest, OnchainReceiveResponse,
OnchainSendRequest, OnchainSendResponse, OpenChannelRequest, OpenChannelResponse,
};
use ldk_server_protos::error::{ErrorCode, ErrorResponse};
use reqwest::header::CONTENT_TYPE;
use reqwest::Client;

Expand Down Expand Up @@ -152,27 +156,46 @@ impl LdkServerClient {
&self, request: &Rq, url: &str,
) -> Result<Rs, LdkServerError> {
let request_body = request.encode_to_vec();
let response_raw = match self
let response_raw = self
.client
.post(url)
.header(CONTENT_TYPE, APPLICATION_OCTET_STREAM)
.body(request_body)
.send()
.await
{
Ok(response) => response,
Err(e) => {
return Err(LdkServerError::InternalError(e.to_string()));
},
};
.map_err(|e| {
LdkServerError::new(InternalError, format!("HTTP request failed: {}", e))
})?;

let status = response_raw.status();
let payload = response_raw.bytes().await?;
let payload = response_raw.bytes().await.map_err(|e| {
LdkServerError::new(InternalError, format!("Failed to read response body: {}", e))
})?;

if status.is_success() {
Ok(Rs::decode(&payload[..])?)
Ok(Rs::decode(&payload[..]).map_err(|e| {
LdkServerError::new(
InternalError,
format!("Failed to decode success response: {}", e),
)
})?)
} else {
//TODO: Error handling and error response parsing.
Err(LdkServerError::InternalError("Unknown Error".to_string()))
let error_response = ErrorResponse::decode(&payload[..]).map_err(|e| {
LdkServerError::new(
InternalError,
format!("Failed to decode error response (status {}): {}", status, e),
)
})?;

let error_code = match ErrorCode::from_i32(error_response.error_code) {
Some(ErrorCode::InvalidRequestError) => InvalidRequestError,
Some(ErrorCode::AuthError) => AuthError,
Some(ErrorCode::LightningError) => LightningError,
Some(ErrorCode::InternalServerError) => InternalServerError,
Some(ErrorCode::UnknownError) | None => InternalError,
};

Err(LdkServerError::new(error_code, error_response.message))
}
}
}
66 changes: 54 additions & 12 deletions ldk-server-client/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,62 @@
use prost::DecodeError;
use std::fmt;

/// When there is an error in request to LDK Server, the response contains a relevant error code.
#[derive(Debug)]
pub enum LdkServerError {
/// There is an unknown error. (Placeholder until error handling is done.)
InternalError(String),
/// Represents an error returned by the LDK server.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LdkServerError {
/// The error message containing a generic description of the error condition in English.
/// It is intended for a human audience only and should not be parsed to extract any information
/// programmatically. Client-side code may use it for logging only.
pub message: String,

/// The error code uniquely identifying an error condition.
/// It is meant to be read and understood programmatically by code that detects/handles errors by
/// type.
pub error_code: LdkServerErrorCode,
}

impl LdkServerError {
/// Creates a new [`LdkServerError`] with the given error code and message.
pub fn new(error_code: LdkServerErrorCode, message: impl Into<String>) -> Self {
Self { error_code, message: message.into() }
}
}

impl From<DecodeError> for LdkServerError {
fn from(err: DecodeError) -> Self {
LdkServerError::InternalError(err.to_string())
impl std::error::Error for LdkServerError {}

impl fmt::Display for LdkServerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Error: [{}]: {}", self.error_code, self.message)
}
}

impl From<reqwest::Error> for LdkServerError {
fn from(err: reqwest::Error) -> Self {
LdkServerError::InternalError(err.to_string())
/// Defines error codes for categorizing LDK server errors.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LdkServerErrorCode {
/// Please refer to [`ldk_server_protos::error::ErrorCode::InvalidRequestError`].
InvalidRequestError,

/// Please refer to [`ldk_server_protos::error::ErrorCode::AuthError`].
AuthError,

/// Please refer to [`ldk_server_protos::error::ErrorCode::LightningError`].
LightningError,

/// Please refer to [`ldk_server_protos::error::ErrorCode::InternalServerError`].
InternalServerError,

/// There is an unknown error, it could be a client-side bug, unrecognized error-code, network error
/// or something else.
InternalError,
}

impl fmt::Display for LdkServerErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LdkServerErrorCode::InvalidRequestError => write!(f, "InvalidRequestError"),
LdkServerErrorCode::AuthError => write!(f, "AuthError"),
LdkServerErrorCode::LightningError => write!(f, "LightningError"),
LdkServerErrorCode::InternalServerError => write!(f, "InternalServerError"),
LdkServerErrorCode::InternalError => write!(f, "InternalError"),
}
}
}
3 changes: 2 additions & 1 deletion ldk-server/src/api/bolt11_receive.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::api::error::LdkServerError;
use crate::service::Context;
use crate::util::proto_adapter::proto_to_bolt11_description;
use ldk_server_protos::api::{Bolt11ReceiveRequest, Bolt11ReceiveResponse};
Expand All @@ -6,7 +7,7 @@ pub(crate) const BOLT11_RECEIVE_PATH: &str = "Bolt11Receive";

pub(crate) fn handle_bolt11_receive_request(
context: Context, request: Bolt11ReceiveRequest,
) -> Result<Bolt11ReceiveResponse, ldk_node::NodeError> {
) -> Result<Bolt11ReceiveResponse, LdkServerError> {
let description = proto_to_bolt11_description(request.description)?;
let invoice = match request.amount_msat {
Some(amount_msat) => {
Expand Down
3 changes: 2 additions & 1 deletion ldk-server/src/api/bolt11_send.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::api::error::LdkServerError;
use crate::service::Context;
use bytes::Bytes;
use ldk_node::lightning_invoice::Bolt11Invoice;
Expand All @@ -8,7 +9,7 @@ pub(crate) const BOLT11_SEND_PATH: &str = "Bolt11Send";

pub(crate) fn handle_bolt11_send_request(
context: Context, request: Bolt11SendRequest,
) -> Result<Bolt11SendResponse, ldk_node::NodeError> {
) -> Result<Bolt11SendResponse, LdkServerError> {
let invoice = Bolt11Invoice::from_str(&request.invoice.as_str())
.map_err(|_| ldk_node::NodeError::InvalidInvoice)?;

Expand Down
3 changes: 2 additions & 1 deletion ldk-server/src/api/bolt12_receive.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use crate::api::error::LdkServerError;
use crate::service::Context;
use ldk_server_protos::api::{Bolt12ReceiveRequest, Bolt12ReceiveResponse};

pub(crate) const BOLT12_RECEIVE_PATH: &str = "Bolt12Receive";

pub(crate) fn handle_bolt12_receive_request(
context: Context, request: Bolt12ReceiveRequest,
) -> Result<Bolt12ReceiveResponse, ldk_node::NodeError> {
) -> Result<Bolt12ReceiveResponse, LdkServerError> {
let offer = match request.amount_msat {
Some(amount_msat) => context.node.bolt12_payment().receive(
amount_msat,
Expand Down
3 changes: 2 additions & 1 deletion ldk-server/src/api/bolt12_send.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::api::error::LdkServerError;
use crate::service::Context;
use bytes::Bytes;
use ldk_node::lightning::offers::offer::Offer;
Expand All @@ -8,7 +9,7 @@ pub(crate) const BOLT12_SEND_PATH: &str = "Bolt12Send";

pub(crate) fn handle_bolt12_send_request(
context: Context, request: Bolt12SendRequest,
) -> Result<Bolt12SendResponse, ldk_node::NodeError> {
) -> Result<Bolt12SendResponse, LdkServerError> {
let offer =
Offer::from_str(&request.offer.as_str()).map_err(|_| ldk_node::NodeError::InvalidOffer)?;

Expand Down
12 changes: 9 additions & 3 deletions ldk-server/src/api/close_channel.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use crate::api::error::LdkServerError;
use crate::api::error::LdkServerErrorCode::InvalidRequestError;
use crate::service::Context;
use ldk_node::bitcoin::secp256k1::PublicKey;
use ldk_node::UserChannelId;
Expand All @@ -8,13 +10,17 @@ pub(crate) const CLOSE_CHANNEL_PATH: &str = "CloseChannel";

pub(crate) fn handle_close_channel_request(
context: Context, request: CloseChannelRequest,
) -> Result<CloseChannelResponse, ldk_node::NodeError> {
) -> Result<CloseChannelResponse, LdkServerError> {
//TODO: Should this be string?
let mut user_channel_id_bytes = [0u8; 16];
user_channel_id_bytes.copy_from_slice(&request.user_channel_id);
let user_channel_id = UserChannelId(u128::from_be_bytes(user_channel_id_bytes));
let counterparty_node_id = PublicKey::from_str(&request.counterparty_node_id)
.map_err(|_| ldk_node::NodeError::InvalidPublicKey)?;
let counterparty_node_id = PublicKey::from_str(&request.counterparty_node_id).map_err(|e| {
LdkServerError::new(
InvalidRequestError,
format!("Invalid counterparty node ID, error: {}", e),
)
})?;

match request.force_close {
Some(true) => context.node.force_close_channel(
Expand Down
94 changes: 94 additions & 0 deletions ldk-server/src/api/error.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
use ldk_node::NodeError;
use std::fmt;

#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct LdkServerError {
// The error message containing a generic description of the error condition in English.
// It is intended for a human audience only and should not be parsed to extract any information
Expand All @@ -10,6 +14,21 @@ pub(crate) struct LdkServerError {
pub(crate) error_code: LdkServerErrorCode,
}

impl LdkServerError {
pub(crate) fn new(error_code: LdkServerErrorCode, message: impl Into<String>) -> Self {
Self { error_code, message: message.into() }
}
}

impl std::error::Error for LdkServerError {}

impl fmt::Display for LdkServerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Error: [{}]: {}", self.error_code, self.message)
}
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum LdkServerErrorCode {
/// Please refer to [`protos::error::ErrorCode::InvalidRequestError`].
InvalidRequestError,
Expand All @@ -23,3 +42,78 @@ pub(crate) enum LdkServerErrorCode {
/// Please refer to [`protos::error::ErrorCode::InternalServerError`].
InternalServerError,
}

impl fmt::Display for LdkServerErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LdkServerErrorCode::InvalidRequestError => write!(f, "InvalidRequestError"),
LdkServerErrorCode::AuthError => write!(f, "AuthError"),
LdkServerErrorCode::LightningError => write!(f, "LightningError"),
LdkServerErrorCode::InternalServerError => write!(f, "InternalServerError"),
}
}
}

impl From<NodeError> for LdkServerError {
fn from(error: NodeError) -> Self {
let (message, error_code) = match error {
NodeError::InvalidAddress
| NodeError::InvalidSocketAddress
| NodeError::InvalidPublicKey
| NodeError::InvalidSecretKey
| NodeError::InvalidOfferId
| NodeError::InvalidNodeId
| NodeError::InvalidPaymentId
| NodeError::InvalidPaymentHash
| NodeError::InvalidPaymentPreimage
| NodeError::InvalidPaymentSecret
| NodeError::InvalidAmount
| NodeError::InvalidInvoice
| NodeError::InvalidOffer
| NodeError::InvalidRefund
| NodeError::InvalidChannelId
| NodeError::InvalidNetwork
| NodeError::InvalidUri
| NodeError::InvalidQuantity
| NodeError::InvalidNodeAlias
| NodeError::InvalidDateTime
| NodeError::InvalidFeeRate
| NodeError::UriParameterParsingFailed => {
(error.to_string(), LdkServerErrorCode::InvalidRequestError)
},

NodeError::ConnectionFailed
| NodeError::InvoiceCreationFailed
| NodeError::InvoiceRequestCreationFailed
| NodeError::OfferCreationFailed
| NodeError::RefundCreationFailed
| NodeError::PaymentSendingFailed
| NodeError::InvalidCustomTlvs
| NodeError::ProbeSendingFailed
| NodeError::ChannelCreationFailed
| NodeError::ChannelClosingFailed
| NodeError::ChannelConfigUpdateFailed
| NodeError::DuplicatePayment
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't looked through these exhaustively, but for BOLT11 DuplicatePayment is returned if the user tries to pay an invoice with the same payment hash twice. So shouldn't it map to an InvalidRequestError? Though for BOLT12 it would happen if we randomly generate the same payment id, which shouldn't happen.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't mark DuplicatePayment as invalid-request with certainty, it could be a legitimate race-condition on client side or idempotency failure.
Ideally, DuplicatePayment should become a sub-error within the lightning error category.

We typically use invalid-request for requests that can be safely ignored or can’t be fixed programmatically, but DuplicatePayment might indicate a successful payment the client didn’t register.

| NodeError::InsufficientFunds
| NodeError::UnsupportedCurrency
| NodeError::LiquidityFeeTooHigh => (error.to_string(), LdkServerErrorCode::LightningError),

NodeError::AlreadyRunning
| NodeError::NotRunning
| NodeError::PersistenceFailed
| NodeError::FeerateEstimationUpdateFailed
| NodeError::FeerateEstimationUpdateTimeout
| NodeError::WalletOperationFailed
| NodeError::WalletOperationTimeout
| NodeError::GossipUpdateFailed
| NodeError::GossipUpdateTimeout
| NodeError::LiquiditySourceUnavailable
| NodeError::LiquidityRequestFailed
| NodeError::OnchainTxCreationFailed
| NodeError::OnchainTxSigningFailed
| NodeError::TxSyncFailed
| NodeError::TxSyncTimeout => (error.to_string(), LdkServerErrorCode::InternalServerError),
};
LdkServerError::new(error_code, message)
}
}
Loading