breez_sdk_core/greenlight/
error.rs

1use std::{num::TryFromIntError, time::SystemTimeError};
2
3use anyhow::{anyhow, Result};
4use regex::Regex;
5use strum_macros::FromRepr;
6
7use crate::{bitcoin::secp256k1, node_api::NodeError};
8
9#[derive(FromRepr, Debug, PartialEq)]
10#[repr(i16)]
11pub(crate) enum JsonRpcErrCode {
12    /* Errors from `pay`, `sendpay`, or `waitsendpay` commands */
13    PayInProgress = 200,
14    PayRhashAlreadyUsed = 201,
15    PayUnparseableOnion = 202,
16    PayDestinationPermFail = 203,
17    PayTryOtherRoute = 204,
18    PayRouteNotFound = 205,
19    PayRouteTooExpensive = 206,
20    PayInvoiceExpired = 207,
21    PayNoSuchPayment = 208,
22    PayUnspecifiedError = 209,
23    PayStoppedRetrying = 210,
24    PayStatusUnexpected = 211,
25    PayInvoiceRequestInvalid = 212,
26    PayInvoicePreapprovalDeclined = 213,
27    PayKeysendPreapprovalDeclined = 214,
28
29    /* `fundchannel` or `withdraw` errors */
30    FundMaxExceeded = 300,
31    FundCannotAfford = 301,
32    FundOutputIsDust = 302,
33    FundingBroadcastFail = 303,
34    FundingStillSyncingBitcoin = 304,
35    FundingPeerNotConnected = 305,
36    FundingUnknownPeer = 306,
37    FundingNothingToCancel = 307,
38    FundingCancelNotSafe = 308,
39    FundingPsbtInvalid = 309,
40    FundingV2NotSupported = 310,
41    FundingUnknownChannel = 311,
42    FundingStateInvalid = 312,
43    FundCannotAffordWithEmergency = 313,
44
45    /* Splice errors */
46    SpliceBroadcastFail = 350,
47    SpliceWrongOwner = 351,
48    SpliceUnknownChannel = 352,
49    SpliceInvalidChannelState = 353,
50    SpliceNotSupported = 354,
51    SpliceBusyError = 355,
52    SpliceInputError = 356,
53    SpliceFundingLow = 357,
54    SpliceStateError = 358,
55    SpliceLowFee = 359,
56    SpliceHighFee = 360,
57
58    /* `connect` errors */
59    ConnectNoKnownAddress = 400,
60    ConnectAllAddressesFailed = 401,
61    ConnectDisconnectedDuring = 402,
62
63    /* bitcoin-cli plugin errors */
64    BcliError = 500,
65    BcliNoFeeEstimates = 501,
66
67    /* Errors from `invoice` or `delinvoice` commands */
68    InvoiceLabelAlreadyExists = 900,
69    InvoicePreimageAlreadyExists = 901,
70    InvoiceHintsGaveNoRoutes = 902,
71    InvoiceExpiredDuringWait = 903,
72    InvoiceWaitTimedOut = 904,
73    InvoiceNotFound = 905,
74    InvoiceStatusUnexpected = 906,
75    InvoiceOfferInactive = 907,
76    InvoiceNoDescription = 908,
77
78    /* Errors from HSM crypto operations. */
79    HsmEcdhFailed = 800,
80
81    /* Errors from `offer` commands */
82    OfferAlreadyExists = 1000,
83    OfferAlreadyDisabled = 1001,
84    OfferExpired = 1002,
85    OfferRouteNotFound = 1003,
86    OfferBadInvreqReply = 1004,
87    OfferTimeout = 1005,
88
89    /* Errors from datastore command */
90    DatastoreDelDoesNotExist = 1200,
91    DatastoreDelWrongGeneration = 1201,
92    DatastoreUpdateAlreadyExists = 1202,
93    DatastoreUpdateDoesNotExist = 1203,
94    DatastoreUpdateWrongGeneration = 1204,
95    DatastoreUpdateHasChildren = 1205,
96    DatastoreUpdateNoChildren = 1206,
97
98    /* Errors from signmessage command */
99    SignmessagePubkeyNotFound = 1301,
100
101    /* Errors from delforward command */
102    DelforwardNotFound = 1401,
103
104    /* Errors from runes */
105    RuneNotAuthorized = 1501,
106    RuneNotPermitted = 1502,
107    RuneBlacklisted = 1503,
108
109    /* Errors from wait* commands */
110    WaitTimeout = 2000,
111}
112
113impl From<anyhow::Error> for NodeError {
114    fn from(err: anyhow::Error) -> Self {
115        Self::Generic(err.to_string())
116    }
117}
118
119impl From<crate::bitcoin::util::address::Error> for NodeError {
120    fn from(err: crate::bitcoin::util::address::Error) -> Self {
121        Self::Generic(err.to_string())
122    }
123}
124
125impl From<hex::FromHexError> for NodeError {
126    fn from(err: hex::FromHexError) -> Self {
127        Self::Generic(err.to_string())
128    }
129}
130
131impl From<secp256k1::Error> for NodeError {
132    fn from(err: secp256k1::Error) -> Self {
133        Self::Generic(err.to_string())
134    }
135}
136
137impl From<serde_json::Error> for NodeError {
138    fn from(err: serde_json::Error) -> Self {
139        Self::Generic(err.to_string())
140    }
141}
142
143impl From<SystemTimeError> for NodeError {
144    fn from(err: SystemTimeError) -> Self {
145        Self::Generic(err.to_string())
146    }
147}
148
149impl From<tonic::Status> for NodeError {
150    fn from(status: tonic::Status) -> Self {
151        let wrapped_status = sdk_common::tonic_wrap::Status(status.clone());
152        match parse_cln_error(status) {
153            Ok(code) => match code {
154                // Pay errors
155                JsonRpcErrCode::PayInvoiceExpired => {
156                    Self::InvoiceExpired(wrapped_status.to_string())
157                }
158                JsonRpcErrCode::PayTryOtherRoute | JsonRpcErrCode::PayRouteNotFound => {
159                    Self::RouteNotFound(wrapped_status.to_string())
160                }
161                JsonRpcErrCode::PayRouteTooExpensive => {
162                    Self::RouteTooExpensive(wrapped_status.to_string())
163                }
164                JsonRpcErrCode::PayStoppedRetrying => {
165                    Self::PaymentTimeout(wrapped_status.to_string())
166                }
167                JsonRpcErrCode::PayRhashAlreadyUsed
168                | JsonRpcErrCode::PayUnparseableOnion
169                | JsonRpcErrCode::PayDestinationPermFail
170                | JsonRpcErrCode::PayNoSuchPayment
171                | JsonRpcErrCode::PayUnspecifiedError
172                | JsonRpcErrCode::PayStatusUnexpected
173                | JsonRpcErrCode::PayInvoiceRequestInvalid
174                | JsonRpcErrCode::PayInvoicePreapprovalDeclined
175                | JsonRpcErrCode::PayKeysendPreapprovalDeclined => {
176                    Self::PaymentFailed(wrapped_status.to_string())
177                }
178                // Invoice errors
179                JsonRpcErrCode::InvoiceExpiredDuringWait => {
180                    Self::InvoiceExpired(wrapped_status.to_string())
181                }
182                JsonRpcErrCode::InvoiceNoDescription => {
183                    Self::InvoiceNoDescription(wrapped_status.to_string())
184                }
185                JsonRpcErrCode::InvoicePreimageAlreadyExists => {
186                    Self::InvoicePreimageAlreadyExists(wrapped_status.to_string())
187                }
188                _ => Self::Generic(wrapped_status.to_string()),
189            },
190            _ => Self::Generic(wrapped_status.to_string()),
191        }
192    }
193}
194
195impl From<TryFromIntError> for NodeError {
196    fn from(err: TryFromIntError) -> Self {
197        Self::Generic(err.to_string())
198    }
199}
200
201impl From<gl_client::credentials::Error> for NodeError {
202    fn from(value: gl_client::credentials::Error) -> Self {
203        NodeError::Credentials(value.to_string())
204    }
205}
206
207#[allow(clippy::invalid_regex)]
208pub(crate) fn parse_cln_error(status: tonic::Status) -> Result<JsonRpcErrCode> {
209    let re: Regex = Regex::new(r"Some\((?<code>-?\d+)\)")?;
210    re.captures(status.message())
211        .and_then(|caps| {
212            caps["code"]
213                .parse::<i16>()
214                .map_or(None, JsonRpcErrCode::from_repr)
215        })
216        .ok_or(anyhow!("No code found"))
217        .or(parse_cln_error_wrapped(status))
218}
219
220/// Try to parse and extract the status code from nested tonic statuses.
221///
222/// ```ignore
223/// Example: Generic: Generic: status: Internal, message: \"converting invoice response to grpc:
224/// error calling RPC: RPC error response: RpcError { code: 901, message: \\\"preimage already used\\\",
225/// data: None }\", details: [], metadata: MetadataMap { headers: {\"content-type\": \"application/grpc\",
226/// \"date\": \"Thu, 08 Feb 2024 20:57:17 GMT\", \"content-length\": \"0\"} }
227/// ```
228///
229/// The [tonic::Status] is nested into an [tonic::Code::Internal] one here:
230/// <https://github.com/Blockstream/greenlight/blob/e87f60e473edf9395631086c48ba6234c0c052ff/libs/gl-plugin/src/node/wrapper.rs#L90-L93>
231pub(crate) fn parse_cln_error_wrapped(status: tonic::Status) -> Result<JsonRpcErrCode> {
232    let re: Regex = Regex::new(r"code:? (?<code>-?\d+)")?;
233    re.captures(status.message())
234        .and_then(|caps| {
235            caps["code"]
236                .parse::<i16>()
237                .map_or(None, JsonRpcErrCode::from_repr)
238        })
239        .ok_or(anyhow!("No code found"))
240}
241
242#[cfg(test)]
243mod tests {
244    use anyhow::Result;
245    use tonic::Code;
246
247    use crate::greenlight::error::{parse_cln_error, parse_cln_error_wrapped, JsonRpcErrCode};
248
249    #[test]
250    fn test_parse_cln_error() -> Result<()> {
251        assert!(matches!(
252            parse_cln_error(tonic::Status::new(
253                Code::Internal,
254                "converting invoice response to grpc: Error code 901: preimage already used"
255            )),
256            Ok(JsonRpcErrCode::InvoicePreimageAlreadyExists)
257        ));
258
259        assert!(parse_cln_error(tonic::Status::new(Code::Internal, "...")).is_err());
260
261        assert!(matches!(
262            parse_cln_error(tonic::Status::new(Code::Internal, "... Some(208) ...")),
263            Ok(JsonRpcErrCode::PayNoSuchPayment)
264        ));
265
266        assert!(matches!(
267            parse_cln_error(tonic::Status::new(Code::Internal, "... Some(901) ...")),
268            Ok(JsonRpcErrCode::InvoicePreimageAlreadyExists)
269        ));
270
271        // Test if it falls back to parsing the nested status
272        assert!(matches!(
273            parse_cln_error(tonic::Status::new(
274                Code::Internal,
275                "... RpcError { code: 901, message ... } ..."
276            )),
277            Ok(JsonRpcErrCode::InvoicePreimageAlreadyExists)
278        ));
279
280        Ok(())
281    }
282
283    #[test]
284    fn test_parse_cln_error_wrapped() -> Result<()> {
285        assert!(parse_cln_error_wrapped(tonic::Status::new(Code::Internal, "...")).is_err());
286
287        assert!(matches!(
288            parse_cln_error_wrapped(tonic::Status::new(
289                Code::Internal,
290                "... RpcError { code: 208, message ... } ..."
291            )),
292            Ok(JsonRpcErrCode::PayNoSuchPayment)
293        ));
294
295        assert!(matches!(
296            parse_cln_error_wrapped(tonic::Status::new(
297                Code::Internal,
298                "... RpcError { code: 901, message ... } ..."
299            )),
300            Ok(JsonRpcErrCode::InvoicePreimageAlreadyExists)
301        ));
302
303        Ok(())
304    }
305}