breez_sdk_core/greenlight/
error.rs1use 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 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 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 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 ConnectNoKnownAddress = 400,
60 ConnectAllAddressesFailed = 401,
61 ConnectDisconnectedDuring = 402,
62
63 BcliError = 500,
65 BcliNoFeeEstimates = 501,
66
67 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 HsmEcdhFailed = 800,
80
81 OfferAlreadyExists = 1000,
83 OfferAlreadyDisabled = 1001,
84 OfferExpired = 1002,
85 OfferRouteNotFound = 1003,
86 OfferBadInvreqReply = 1004,
87 OfferTimeout = 1005,
88
89 DatastoreDelDoesNotExist = 1200,
91 DatastoreDelWrongGeneration = 1201,
92 DatastoreUpdateAlreadyExists = 1202,
93 DatastoreUpdateDoesNotExist = 1203,
94 DatastoreUpdateWrongGeneration = 1204,
95 DatastoreUpdateHasChildren = 1205,
96 DatastoreUpdateNoChildren = 1206,
97
98 SignmessagePubkeyNotFound = 1301,
100
101 DelforwardNotFound = 1401,
103
104 RuneNotAuthorized = 1501,
106 RuneNotPermitted = 1502,
107 RuneBlacklisted = 1503,
108
109 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 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 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
220pub(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 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}