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<crate::bitcoin::util::bip32::Error> for NodeError {
126    fn from(err: crate::bitcoin::util::bip32::Error) -> Self {
127        Self::Generic(err.to_string())
128    }
129}
130
131impl From<hex::FromHexError> for NodeError {
132    fn from(err: hex::FromHexError) -> Self {
133        Self::Generic(err.to_string())
134    }
135}
136
137impl From<secp256k1::Error> for NodeError {
138    fn from(err: secp256k1::Error) -> Self {
139        Self::Generic(err.to_string())
140    }
141}
142
143impl From<serde_json::Error> for NodeError {
144    fn from(err: serde_json::Error) -> Self {
145        Self::Generic(err.to_string())
146    }
147}
148
149impl From<SystemTimeError> for NodeError {
150    fn from(err: SystemTimeError) -> Self {
151        Self::Generic(err.to_string())
152    }
153}
154
155impl From<tonic::Status> for NodeError {
156    fn from(status: tonic::Status) -> Self {
157        let wrapped_status = sdk_common::tonic_wrap::Status(status.clone());
158        match parse_cln_error(status) {
159            Ok(code) => match code {
160                // Pay errors
161                JsonRpcErrCode::PayInvoiceExpired => {
162                    Self::InvoiceExpired(wrapped_status.to_string())
163                }
164                JsonRpcErrCode::PayTryOtherRoute | JsonRpcErrCode::PayRouteNotFound => {
165                    Self::RouteNotFound(wrapped_status.to_string())
166                }
167                JsonRpcErrCode::PayRouteTooExpensive => {
168                    Self::RouteTooExpensive(wrapped_status.to_string())
169                }
170                JsonRpcErrCode::PayStoppedRetrying => {
171                    Self::PaymentTimeout(wrapped_status.to_string())
172                }
173                JsonRpcErrCode::PayRhashAlreadyUsed
174                | JsonRpcErrCode::PayUnparseableOnion
175                | JsonRpcErrCode::PayDestinationPermFail
176                | JsonRpcErrCode::PayNoSuchPayment
177                | JsonRpcErrCode::PayUnspecifiedError
178                | JsonRpcErrCode::PayStatusUnexpected
179                | JsonRpcErrCode::PayInvoiceRequestInvalid
180                | JsonRpcErrCode::PayInvoicePreapprovalDeclined
181                | JsonRpcErrCode::PayKeysendPreapprovalDeclined => {
182                    Self::PaymentFailed(wrapped_status.to_string())
183                }
184                // Invoice errors
185                JsonRpcErrCode::InvoiceExpiredDuringWait => {
186                    Self::InvoiceExpired(wrapped_status.to_string())
187                }
188                JsonRpcErrCode::InvoiceNoDescription => {
189                    Self::InvoiceNoDescription(wrapped_status.to_string())
190                }
191                JsonRpcErrCode::InvoicePreimageAlreadyExists => {
192                    Self::InvoicePreimageAlreadyExists(wrapped_status.to_string())
193                }
194                _ => Self::Generic(wrapped_status.to_string()),
195            },
196            _ => Self::Generic(wrapped_status.to_string()),
197        }
198    }
199}
200
201impl From<TryFromIntError> for NodeError {
202    fn from(err: TryFromIntError) -> Self {
203        Self::Generic(err.to_string())
204    }
205}
206
207impl From<gl_client::credentials::Error> for NodeError {
208    fn from(value: gl_client::credentials::Error) -> Self {
209        NodeError::Credentials(value.to_string())
210    }
211}
212
213#[allow(clippy::invalid_regex)]
214pub(crate) fn parse_cln_error(status: tonic::Status) -> Result<JsonRpcErrCode> {
215    let re: Regex = Regex::new(r"Some\((?<code>-?\d+)\)")?;
216    re.captures(status.message())
217        .and_then(|caps| {
218            caps["code"]
219                .parse::<i16>()
220                .map_or(None, JsonRpcErrCode::from_repr)
221        })
222        .ok_or(anyhow!("No code found"))
223        .or(parse_cln_error_wrapped(status))
224}
225
226/// Try to parse and extract the status code from nested tonic statuses.
227///
228/// ```ignore
229/// Example: Generic: Generic: status: Internal, message: \"converting invoice response to grpc:
230/// error calling RPC: RPC error response: RpcError { code: 901, message: \\\"preimage already used\\\",
231/// data: None }\", details: [], metadata: MetadataMap { headers: {\"content-type\": \"application/grpc\",
232/// \"date\": \"Thu, 08 Feb 2024 20:57:17 GMT\", \"content-length\": \"0\"} }
233/// ```
234///
235/// The [tonic::Status] is nested into an [tonic::Code::Internal] one here:
236/// <https://github.com/Blockstream/greenlight/blob/e87f60e473edf9395631086c48ba6234c0c052ff/libs/gl-plugin/src/node/wrapper.rs#L90-L93>
237pub(crate) fn parse_cln_error_wrapped(status: tonic::Status) -> Result<JsonRpcErrCode> {
238    let re: Regex = Regex::new(r"code: (?<code>-?\d+)")?;
239    re.captures(status.message())
240        .and_then(|caps| {
241            caps["code"]
242                .parse::<i16>()
243                .map_or(None, JsonRpcErrCode::from_repr)
244        })
245        .ok_or(anyhow!("No code found"))
246}
247
248#[cfg(test)]
249mod tests {
250    use anyhow::Result;
251    use tonic::Code;
252
253    use crate::greenlight::error::{parse_cln_error, parse_cln_error_wrapped, JsonRpcErrCode};
254
255    #[test]
256    fn test_parse_cln_error() -> Result<()> {
257        assert!(parse_cln_error(tonic::Status::new(Code::Internal, "...")).is_err());
258
259        assert!(matches!(
260            parse_cln_error(tonic::Status::new(Code::Internal, "... Some(208) ...")),
261            Ok(JsonRpcErrCode::PayNoSuchPayment)
262        ));
263
264        assert!(matches!(
265            parse_cln_error(tonic::Status::new(Code::Internal, "... Some(901) ...")),
266            Ok(JsonRpcErrCode::InvoicePreimageAlreadyExists)
267        ));
268
269        // Test if it falls back to parsing the nested status
270        assert!(matches!(
271            parse_cln_error(tonic::Status::new(
272                Code::Internal,
273                "... RpcError { code: 901, message ... } ..."
274            )),
275            Ok(JsonRpcErrCode::InvoicePreimageAlreadyExists)
276        ));
277
278        Ok(())
279    }
280
281    #[test]
282    fn test_parse_cln_error_wrapped() -> Result<()> {
283        assert!(parse_cln_error_wrapped(tonic::Status::new(Code::Internal, "...")).is_err());
284
285        assert!(matches!(
286            parse_cln_error_wrapped(tonic::Status::new(
287                Code::Internal,
288                "... RpcError { code: 208, message ... } ..."
289            )),
290            Ok(JsonRpcErrCode::PayNoSuchPayment)
291        ));
292
293        assert!(matches!(
294            parse_cln_error_wrapped(tonic::Status::new(
295                Code::Internal,
296                "... RpcError { code: 901, message ... } ..."
297            )),
298            Ok(JsonRpcErrCode::InvoicePreimageAlreadyExists)
299        ));
300
301        Ok(())
302    }
303}