breez_sdk_core/greenlight/
error.rs

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