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