lightning_liquidity/lsps5/
msgs.rs

1// This file is Copyright its original authors, visible in version control
2// history.
3//
4// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
5// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
6// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
7// You may not use this file except in accordance with one or both of these
8// licenses.
9
10//! LSPS5 message formats for webhook registration
11
12use crate::alloc::string::ToString;
13use crate::lsps0::ser::LSPSMessage;
14use crate::lsps0::ser::LSPSRequestId;
15use crate::lsps0::ser::LSPSResponseError;
16
17use super::url_utils::LSPSUrl;
18
19use lightning::ln::msgs::DecodeError;
20use lightning::util::ser::{Readable, Writeable};
21use lightning::{impl_writeable_tlv_based, impl_writeable_tlv_based_enum};
22use lightning_types::string::UntrustedString;
23
24use serde::de::{self, Deserializer, MapAccess, Visitor};
25use serde::ser::SerializeMap;
26use serde::ser::SerializeStruct;
27use serde::Serializer;
28use serde::{Deserialize, Serialize};
29
30use alloc::string::String;
31use alloc::vec::Vec;
32
33use core::fmt;
34use core::fmt::Display;
35use core::ops::Deref;
36
37/// Maximum allowed length for an `app_name` (in bytes).
38pub const MAX_APP_NAME_LENGTH: usize = 64;
39
40/// Maximum allowed length for a webhook URL (in characters).
41pub const MAX_WEBHOOK_URL_LENGTH: usize = 1024;
42
43/// Either the app name or the webhook URL is too long.
44pub const LSPS5_TOO_LONG_ERROR_CODE: i32 = 500;
45/// The provided URL could not be parsed.
46pub const LSPS5_URL_PARSE_ERROR_CODE: i32 = 501;
47/// The provided URL is not HTTPS.
48pub const LSPS5_UNSUPPORTED_PROTOCOL_ERROR_CODE: i32 = 502;
49/// The client has too many webhooks registered.
50pub const LSPS5_TOO_MANY_WEBHOOKS_ERROR_CODE: i32 = 503;
51/// The app name was not found.
52pub const LSPS5_APP_NAME_NOT_FOUND_ERROR_CODE: i32 = 1010;
53/// An unknown error occurred.
54pub const LSPS5_UNKNOWN_ERROR_CODE: i32 = 1000;
55/// An error occurred during serialization of LSPS5 webhook notification.
56pub const LSPS5_SERIALIZATION_ERROR_CODE: i32 = 1001;
57/// A notification was sent too frequently.
58pub const LSPS5_SLOW_DOWN_ERROR_CODE: i32 = 1002;
59/// A request was rejected because the client has no prior activity with the LSP (no open channel and no active LSPS1 or LSPS2 flow). The client should first open a channel
60pub const LSPS5_NO_PRIOR_ACTIVITY_ERROR_CODE: i32 = 1003;
61
62pub(crate) const LSPS5_SET_WEBHOOK_METHOD_NAME: &str = "lsps5.set_webhook";
63pub(crate) const LSPS5_LIST_WEBHOOKS_METHOD_NAME: &str = "lsps5.list_webhooks";
64pub(crate) const LSPS5_REMOVE_WEBHOOK_METHOD_NAME: &str = "lsps5.remove_webhook";
65
66pub(crate) const LSPS5_WEBHOOK_REGISTERED_NOTIFICATION: &str = "lsps5.webhook_registered";
67pub(crate) const LSPS5_PAYMENT_INCOMING_NOTIFICATION: &str = "lsps5.payment_incoming";
68pub(crate) const LSPS5_EXPIRY_SOON_NOTIFICATION: &str = "lsps5.expiry_soon";
69pub(crate) const LSPS5_LIQUIDITY_MANAGEMENT_REQUEST_NOTIFICATION: &str =
70	"lsps5.liquidity_management_request";
71pub(crate) const LSPS5_ONION_MESSAGE_INCOMING_NOTIFICATION: &str = "lsps5.onion_message_incoming";
72
73/// Protocol errors defined in the LSPS5/bLIP-55 specification.
74///
75/// These errors are sent over JSON-RPC when protocol-level validation fails
76/// and correspond directly to error codes defined in the LSPS5 specification.
77/// LSPs must use these errors when rejecting client requests.
78#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
79pub enum LSPS5ProtocolError {
80	/// App name exceeds the maximum allowed length of 64 bytes.
81	///
82	/// Sent when registering a webhook with an app name longer than MAX_APP_NAME_LENGTH.
83	AppNameTooLong,
84
85	/// Webhook URL exceeds the maximum allowed length of 1024 bytes.
86	///
87	/// Sent when registering a webhook with a URL longer than MAX_WEBHOOK_URL_LENGTH.
88	WebhookUrlTooLong,
89
90	/// Webhook URL is not a valid URL.
91	///
92	/// Sent when the provided webhook URL cannot be parsed or is syntactically invalid.
93	UrlParse,
94
95	/// Webhook URL does not use HTTPS.
96	///
97	/// The LSPS5 specification requires all webhook URLs to use HTTPS.
98	UnsupportedProtocol,
99
100	/// Client has reached their maximum allowed number of webhooks.
101	TooManyWebhooks,
102
103	/// The specified app name was not found in the registered webhooks.
104	///
105	/// Sent when trying to update or remove a webhook that doesn't exist.
106	AppNameNotFound,
107
108	/// An unspecified or unexpected error occurred.
109	UnknownError,
110
111	/// Error during serialization of LSPS5 webhook notification.
112	SerializationError,
113
114	/// A notification was sent too frequently.
115	///
116	/// This error indicates that the LSP is sending notifications
117	/// too quickly, violating the notification cooldown [`NOTIFICATION_COOLDOWN_TIME`]
118	///
119	/// [`NOTIFICATION_COOLDOWN_TIME`]: super::service::NOTIFICATION_COOLDOWN_TIME
120	SlowDownError,
121
122	/// Request rejected because the client has no prior activity with the LSP (no open channel and no active LSPS1 or LSPS2 flow). The client should first open a channel
123	/// or initiate an LSPS1/LSPS2 interaction before retrying.
124	NoPriorActivityError,
125}
126
127impl LSPS5ProtocolError {
128	/// The error code for the LSPS5 protocol error.
129	pub fn code(&self) -> i32 {
130		match self {
131			LSPS5ProtocolError::AppNameTooLong | LSPS5ProtocolError::WebhookUrlTooLong => {
132				LSPS5_TOO_LONG_ERROR_CODE
133			},
134			LSPS5ProtocolError::UrlParse => LSPS5_URL_PARSE_ERROR_CODE,
135			LSPS5ProtocolError::UnsupportedProtocol => LSPS5_UNSUPPORTED_PROTOCOL_ERROR_CODE,
136			LSPS5ProtocolError::TooManyWebhooks => LSPS5_TOO_MANY_WEBHOOKS_ERROR_CODE,
137			LSPS5ProtocolError::AppNameNotFound => LSPS5_APP_NAME_NOT_FOUND_ERROR_CODE,
138			LSPS5ProtocolError::UnknownError => LSPS5_UNKNOWN_ERROR_CODE,
139			LSPS5ProtocolError::SerializationError => LSPS5_SERIALIZATION_ERROR_CODE,
140			LSPS5ProtocolError::SlowDownError => LSPS5_SLOW_DOWN_ERROR_CODE,
141			LSPS5ProtocolError::NoPriorActivityError => LSPS5_NO_PRIOR_ACTIVITY_ERROR_CODE,
142		}
143	}
144	/// The error message for the LSPS5 protocol error.
145	pub fn message(&self) -> &'static str {
146		match self {
147			LSPS5ProtocolError::AppNameTooLong => "App name exceeds maximum length",
148			LSPS5ProtocolError::WebhookUrlTooLong => "Webhook URL exceeds maximum length",
149			LSPS5ProtocolError::UrlParse => "Error parsing URL",
150			LSPS5ProtocolError::UnsupportedProtocol => "Unsupported protocol: HTTPS is required",
151			LSPS5ProtocolError::TooManyWebhooks => "Maximum number of webhooks allowed per client",
152			LSPS5ProtocolError::AppNameNotFound => "App name not found",
153			LSPS5ProtocolError::UnknownError => "Unknown error",
154			LSPS5ProtocolError::SerializationError => {
155				"Error serializing LSPS5 webhook notification"
156			},
157			LSPS5ProtocolError::SlowDownError => "Notification sent too frequently",
158			LSPS5ProtocolError::NoPriorActivityError => {
159				"Request rejected due to no prior activity with the LSP"
160			},
161		}
162	}
163}
164
165impl Serialize for LSPS5ProtocolError {
166	fn serialize<S>(&self, ser: S) -> Result<S::Ok, S::Error>
167	where
168		S: Serializer,
169	{
170		let mut s = ser.serialize_struct("error", 2)?;
171		s.serialize_field("code", &self.code())?;
172		s.serialize_field("message", &self.message())?;
173		s.end()
174	}
175}
176
177/// Client-side validation and processing errors.
178///
179/// Unlike LSPS5ProtocolError, these errors are not part of the LSPS5 specification
180/// and are meant for internal use in the client implementation. They represent
181/// failures when parsing, validating, or processing webhook notifications.
182#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
183pub enum LSPS5ClientError {
184	/// Signature verification failed.
185	///
186	/// The cryptographic signature from the LSP node doesn't validate.
187	InvalidSignature,
188
189	/// Detected a reused notification signature.
190	///
191	/// Indicates a potential replay attack where a previously seen
192	/// notification signature was reused.
193	ReplayAttack,
194
195	/// Error during serialization of LSPS5 webhook notification.
196	SerializationError,
197}
198
199impl LSPS5ClientError {
200	const BASE: i32 = 100_000;
201	/// The error code for the client error.
202	pub fn code(&self) -> i32 {
203		use LSPS5ClientError::*;
204		match self {
205			InvalidSignature => Self::BASE + 1,
206			ReplayAttack => Self::BASE + 2,
207			SerializationError => LSPS5_SERIALIZATION_ERROR_CODE,
208		}
209	}
210	/// The error message for the client error.
211	pub fn message(&self) -> &'static str {
212		use LSPS5ClientError::*;
213		match self {
214			InvalidSignature => "Invalid signature",
215			ReplayAttack => "Replay attack detected",
216			SerializationError => "Error serializing LSPS5 webhook notification",
217		}
218	}
219}
220
221#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
222/// Combined error type for LSPS5 client and protocol errors.
223///
224/// This enum wraps both specification-defined protocol errors and
225/// client-side processing errors into a single error type for use
226/// throughout the LSPS5 implementation.
227pub enum LSPS5Error {
228	/// An error defined in the LSPS5 specification.
229	///
230	/// This represents errors that are part of the formal protocol.
231	Protocol(LSPS5ProtocolError),
232
233	/// A client-side processing error.
234	///
235	/// This represents errors that occur during client-side handling
236	/// of notifications or other validation.
237	Client(LSPS5ClientError),
238}
239
240impl From<LSPS5ProtocolError> for LSPS5Error {
241	fn from(e: LSPS5ProtocolError) -> Self {
242		LSPS5Error::Protocol(e)
243	}
244}
245impl From<LSPS5ClientError> for LSPS5Error {
246	fn from(e: LSPS5ClientError) -> Self {
247		LSPS5Error::Client(e)
248	}
249}
250
251impl From<LSPSResponseError> for LSPS5Error {
252	fn from(err: LSPSResponseError) -> Self {
253		LSPS5ProtocolError::from(err).into()
254	}
255}
256
257impl From<LSPSResponseError> for LSPS5ProtocolError {
258	fn from(err: LSPSResponseError) -> Self {
259		match err.code {
260			LSPS5_TOO_LONG_ERROR_CODE => LSPS5ProtocolError::AppNameTooLong,
261			LSPS5_URL_PARSE_ERROR_CODE => LSPS5ProtocolError::UrlParse,
262			LSPS5_UNSUPPORTED_PROTOCOL_ERROR_CODE => LSPS5ProtocolError::UnsupportedProtocol,
263			LSPS5_TOO_MANY_WEBHOOKS_ERROR_CODE => LSPS5ProtocolError::TooManyWebhooks,
264			LSPS5_APP_NAME_NOT_FOUND_ERROR_CODE => LSPS5ProtocolError::AppNameNotFound,
265			LSPS5_SERIALIZATION_ERROR_CODE => LSPS5ProtocolError::SerializationError,
266			LSPS5_SLOW_DOWN_ERROR_CODE => LSPS5ProtocolError::SlowDownError,
267			LSPS5_NO_PRIOR_ACTIVITY_ERROR_CODE => LSPS5ProtocolError::NoPriorActivityError,
268			_ => LSPS5ProtocolError::UnknownError,
269		}
270	}
271}
272
273impl From<LSPS5ProtocolError> for LSPSResponseError {
274	fn from(e: LSPS5ProtocolError) -> Self {
275		LSPSResponseError { code: e.code(), message: e.message().into(), data: None }
276	}
277}
278
279impl From<LSPS5Error> for LSPSResponseError {
280	fn from(e: LSPS5Error) -> Self {
281		match e {
282			LSPS5Error::Protocol(p) => p.into(),
283			LSPS5Error::Client(c) => {
284				LSPSResponseError { code: c.code(), message: c.message().into(), data: None }
285			},
286		}
287	}
288}
289
290/// App name for LSPS5 webhooks.
291#[derive(Debug, Clone, PartialEq, Eq, Hash)]
292pub struct LSPS5AppName(UntrustedString);
293
294impl Writeable for LSPS5AppName {
295	fn write<W: lightning::util::ser::Writer>(
296		&self, writer: &mut W,
297	) -> Result<(), lightning::io::Error> {
298		self.0.write(writer)
299	}
300}
301
302impl Readable for LSPS5AppName {
303	fn read<R: lightning::io::Read>(reader: &mut R) -> Result<Self, DecodeError> {
304		Ok(Self(Readable::read(reader)?))
305	}
306}
307
308impl LSPS5AppName {
309	/// Create a new LSPS5 app name.
310	pub fn new(app_name: String) -> Result<Self, LSPS5Error> {
311		if app_name.len() > MAX_APP_NAME_LENGTH {
312			return Err(LSPS5ProtocolError::AppNameTooLong.into());
313		}
314		Ok(Self(UntrustedString(app_name)))
315	}
316
317	/// Create a new LSPS5 app name from a regular String.
318	pub fn from_string(app_name: String) -> Result<Self, LSPS5Error> {
319		Self::new(app_name)
320	}
321
322	/// Get the app name as a string.
323	pub fn as_str(&self) -> &str {
324		self
325	}
326}
327
328impl Deref for LSPS5AppName {
329	type Target = str;
330
331	fn deref(&self) -> &Self::Target {
332		&self.0 .0
333	}
334}
335
336impl Display for LSPS5AppName {
337	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
338		f.write_str(self)
339	}
340}
341
342impl Serialize for LSPS5AppName {
343	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
344	where
345		S: serde::Serializer,
346	{
347		serializer.serialize_str(self)
348	}
349}
350
351impl<'de> Deserialize<'de> for LSPS5AppName {
352	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
353	where
354		D: serde::Deserializer<'de>,
355	{
356		let s = String::deserialize(deserializer)?;
357		if s.len() > MAX_APP_NAME_LENGTH {
358			return Err(serde::de::Error::custom("App name exceeds maximum length"));
359		}
360		Self::new(s).map_err(|e| serde::de::Error::custom(format!("{:?}", e)))
361	}
362}
363
364impl AsRef<str> for LSPS5AppName {
365	fn as_ref(&self) -> &str {
366		self
367	}
368}
369
370impl From<LSPS5AppName> for String {
371	fn from(app_name: LSPS5AppName) -> Self {
372		app_name.to_string()
373	}
374}
375
376/// URL for LSPS5 webhooks.
377#[derive(Debug, Clone, PartialEq, Eq, Hash)]
378pub struct LSPS5WebhookUrl(LSPSUrl);
379
380impl LSPS5WebhookUrl {
381	/// Create a new LSPS5 webhook URL.
382	pub fn new(url: String) -> Result<Self, LSPS5Error> {
383		if url.len() > MAX_WEBHOOK_URL_LENGTH {
384			return Err(LSPS5ProtocolError::WebhookUrlTooLong.into());
385		}
386		let parsed_url = LSPSUrl::parse(url)?;
387
388		Ok(Self(parsed_url))
389	}
390
391	/// Create a new LSPS5 webhook URL from a regular String.
392	pub fn from_string(url: String) -> Result<Self, LSPS5Error> {
393		Self::new(url)
394	}
395
396	/// Get the webhook URL as a string.
397	pub fn as_str(&self) -> &str {
398		self
399	}
400}
401
402impl Deref for LSPS5WebhookUrl {
403	type Target = str;
404
405	fn deref(&self) -> &Self::Target {
406		self.0.url()
407	}
408}
409
410impl Display for LSPS5WebhookUrl {
411	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
412		f.write_str(self) // Using Deref
413	}
414}
415
416impl Serialize for LSPS5WebhookUrl {
417	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
418	where
419		S: serde::Serializer,
420	{
421		serializer.serialize_str(self)
422	}
423}
424
425impl<'de> Deserialize<'de> for LSPS5WebhookUrl {
426	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
427	where
428		D: serde::Deserializer<'de>,
429	{
430		let s = String::deserialize(deserializer)?;
431		if s.len() > MAX_WEBHOOK_URL_LENGTH {
432			return Err(serde::de::Error::custom("Webhook URL exceeds maximum length"));
433		}
434		Self::new(s).map_err(|e| serde::de::Error::custom(format!("{:?}", e)))
435	}
436}
437
438impl AsRef<str> for LSPS5WebhookUrl {
439	fn as_ref(&self) -> &str {
440		self
441	}
442}
443
444impl From<LSPS5WebhookUrl> for String {
445	fn from(url: LSPS5WebhookUrl) -> Self {
446		url.to_string()
447	}
448}
449
450impl Writeable for LSPS5WebhookUrl {
451	fn write<W: lightning::util::ser::Writer>(
452		&self, writer: &mut W,
453	) -> Result<(), lightning::io::Error> {
454		self.0.write(writer)
455	}
456}
457
458impl Readable for LSPS5WebhookUrl {
459	fn read<R: lightning::io::Read>(reader: &mut R) -> Result<Self, DecodeError> {
460		Ok(Self(Readable::read(reader)?))
461	}
462}
463
464/// Parameters for `lsps5.set_webhook` request.
465#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
466pub struct SetWebhookRequest {
467	/// Human-readable name for the webhook.
468	pub app_name: LSPS5AppName,
469	/// URL of the webhook.
470	pub webhook: LSPS5WebhookUrl,
471}
472
473/// Response for `lsps5.set_webhook`.
474#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
475pub struct SetWebhookResponse {
476	/// Current number of webhooks registered for this client.
477	pub num_webhooks: u32,
478	/// Maximum number of webhooks allowed by LSP.
479	pub max_webhooks: u32,
480	/// Whether this is an unchanged registration.
481	pub no_change: bool,
482}
483
484/// Parameters for `lsps5.list_webhooks` request.
485#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
486pub struct ListWebhooksRequest {}
487
488/// Response for `lsps5.list_webhooks`.
489#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
490pub struct ListWebhooksResponse {
491	/// List of app_names with registered webhooks.
492	pub app_names: Vec<LSPS5AppName>,
493	/// Maximum number of webhooks allowed by LSP.
494	pub max_webhooks: u32,
495}
496
497/// Parameters for `lsps5.remove_webhook` request.
498#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
499pub struct RemoveWebhookRequest {
500	/// App name identifying the webhook to remove.
501	pub app_name: LSPS5AppName,
502}
503
504/// Response for `lsps5.remove_webhook`.
505#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
506pub struct RemoveWebhookResponse {}
507
508/// Webhook notification methods defined in LSPS5.
509#[derive(Debug, Clone, PartialEq, Eq, Hash)]
510pub enum WebhookNotificationMethod {
511	/// Webhook has been successfully registered.
512	LSPS5WebhookRegistered,
513	/// Client has payments pending to be received.
514	LSPS5PaymentIncoming,
515	/// HTLC or time-bound contract is about to expire.
516	LSPS5ExpirySoon {
517		/// Block height when timeout occurs and the LSP would be forced to close the channel
518		timeout: u32,
519	},
520	/// LSP wants to take back some liquidity.
521	LSPS5LiquidityManagementRequest,
522	/// Client has onion messages pending.
523	LSPS5OnionMessageIncoming,
524}
525
526impl_writeable_tlv_based_enum!(WebhookNotificationMethod,
527	(0, LSPS5WebhookRegistered) => {},
528	(2, LSPS5PaymentIncoming) => {},
529	(4, LSPS5ExpirySoon) => {
530		(0, timeout, required),
531	},
532	(6, LSPS5LiquidityManagementRequest) => {},
533	(8, LSPS5OnionMessageIncoming) => {},
534);
535
536/// Webhook notification payload.
537#[derive(Debug, Clone, PartialEq, Eq)]
538pub struct WebhookNotification {
539	/// Notification method with parameters.
540	pub method: WebhookNotificationMethod,
541}
542
543impl WebhookNotification {
544	/// Create a new webhook notification.
545	pub fn new(method: WebhookNotificationMethod) -> Self {
546		Self { method }
547	}
548
549	/// Create a webhook_registered notification.
550	pub fn webhook_registered() -> Self {
551		Self::new(WebhookNotificationMethod::LSPS5WebhookRegistered)
552	}
553
554	/// Create a payment_incoming notification.
555	pub fn payment_incoming() -> Self {
556		Self::new(WebhookNotificationMethod::LSPS5PaymentIncoming)
557	}
558
559	/// Create an expiry_soon notification.
560	pub fn expiry_soon(timeout: u32) -> Self {
561		Self::new(WebhookNotificationMethod::LSPS5ExpirySoon { timeout })
562	}
563
564	/// Create a liquidity_management_request notification.
565	pub fn liquidity_management_request() -> Self {
566		Self::new(WebhookNotificationMethod::LSPS5LiquidityManagementRequest)
567	}
568
569	/// Create an onion_message_incoming notification.
570	pub fn onion_message_incoming() -> Self {
571		Self::new(WebhookNotificationMethod::LSPS5OnionMessageIncoming)
572	}
573}
574
575impl Serialize for WebhookNotification {
576	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
577	where
578		S: serde::Serializer,
579	{
580		let mut map = serializer.serialize_map(Some(3))?;
581		map.serialize_entry("jsonrpc", "2.0")?;
582
583		let method_name = match &self.method {
584			WebhookNotificationMethod::LSPS5WebhookRegistered => {
585				LSPS5_WEBHOOK_REGISTERED_NOTIFICATION
586			},
587			WebhookNotificationMethod::LSPS5PaymentIncoming => LSPS5_PAYMENT_INCOMING_NOTIFICATION,
588			WebhookNotificationMethod::LSPS5ExpirySoon { .. } => LSPS5_EXPIRY_SOON_NOTIFICATION,
589			WebhookNotificationMethod::LSPS5LiquidityManagementRequest => {
590				LSPS5_LIQUIDITY_MANAGEMENT_REQUEST_NOTIFICATION
591			},
592			WebhookNotificationMethod::LSPS5OnionMessageIncoming => {
593				LSPS5_ONION_MESSAGE_INCOMING_NOTIFICATION
594			},
595		};
596		map.serialize_entry("method", &method_name)?;
597
598		let params = match &self.method {
599			WebhookNotificationMethod::LSPS5WebhookRegistered => serde_json::json!({}),
600			WebhookNotificationMethod::LSPS5PaymentIncoming => serde_json::json!({}),
601			WebhookNotificationMethod::LSPS5ExpirySoon { timeout } => {
602				serde_json::json!({ "timeout": timeout })
603			},
604			WebhookNotificationMethod::LSPS5LiquidityManagementRequest => serde_json::json!({}),
605			WebhookNotificationMethod::LSPS5OnionMessageIncoming => serde_json::json!({}),
606		};
607		map.serialize_entry("params", &params)?;
608
609		map.end()
610	}
611}
612
613impl<'de> Deserialize<'de> for WebhookNotification {
614	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
615	where
616		D: Deserializer<'de>,
617	{
618		struct WebhookNotificationVisitor;
619
620		impl<'de> Visitor<'de> for WebhookNotificationVisitor {
621			type Value = WebhookNotification;
622
623			fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
624				formatter.write_str("a valid LSPS5 WebhookNotification object")
625			}
626
627			fn visit_map<V>(self, mut map: V) -> Result<WebhookNotification, V::Error>
628			where
629				V: MapAccess<'de>,
630			{
631				let mut jsonrpc: Option<String> = None;
632				let mut method: Option<String> = None;
633				let mut params: Option<serde_json::Value> = None;
634
635				while let Some(key) = map.next_key::<&str>()? {
636					match key {
637						"jsonrpc" => jsonrpc = Some(map.next_value()?),
638						"method" => method = Some(map.next_value()?),
639						"params" => params = Some(map.next_value()?),
640						_ => {
641							let _: serde::de::IgnoredAny = map.next_value()?;
642						},
643					}
644				}
645
646				let jsonrpc = jsonrpc.ok_or_else(|| de::Error::missing_field("jsonrpc"))?;
647				if jsonrpc != "2.0" {
648					return Err(de::Error::custom("Invalid jsonrpc version"));
649				}
650				let method = method.ok_or_else(|| de::Error::missing_field("method"))?;
651				let params = params.ok_or_else(|| de::Error::missing_field("params"))?;
652
653				let method = match method.as_str() {
654					LSPS5_WEBHOOK_REGISTERED_NOTIFICATION => {
655						WebhookNotificationMethod::LSPS5WebhookRegistered
656					},
657					LSPS5_PAYMENT_INCOMING_NOTIFICATION => {
658						WebhookNotificationMethod::LSPS5PaymentIncoming
659					},
660					LSPS5_EXPIRY_SOON_NOTIFICATION => {
661						if let Some(timeout) = params.get("timeout").and_then(|t| t.as_u64()) {
662							WebhookNotificationMethod::LSPS5ExpirySoon { timeout: timeout as u32 }
663						} else {
664							return Err(de::Error::custom(
665								"Missing or invalid timeout parameter for expiry_soon notification",
666							));
667						}
668					},
669					LSPS5_LIQUIDITY_MANAGEMENT_REQUEST_NOTIFICATION => {
670						WebhookNotificationMethod::LSPS5LiquidityManagementRequest
671					},
672					LSPS5_ONION_MESSAGE_INCOMING_NOTIFICATION => {
673						WebhookNotificationMethod::LSPS5OnionMessageIncoming
674					},
675					_ => return Err(de::Error::custom(format!("Unknown method: {}", method))),
676				};
677
678				Ok(WebhookNotification { method })
679			}
680		}
681
682		deserializer.deserialize_map(WebhookNotificationVisitor)
683	}
684}
685
686impl_writeable_tlv_based!(WebhookNotification, {
687	(0, method, required),
688});
689
690/// An LSPS5 protocol request.
691#[derive(Clone, Debug, PartialEq, Eq)]
692pub enum LSPS5Request {
693	/// Register or update a webhook.
694	SetWebhook(SetWebhookRequest),
695	/// List all registered webhooks.
696	ListWebhooks(ListWebhooksRequest),
697	/// Remove a webhook.
698	RemoveWebhook(RemoveWebhookRequest),
699}
700
701impl LSPS5Request {
702	pub(crate) fn is_state_allocating(&self) -> bool {
703		matches!(self, LSPS5Request::SetWebhook(_))
704	}
705}
706
707/// An LSPS5 protocol response.
708#[derive(Clone, Debug, PartialEq, Eq)]
709pub enum LSPS5Response {
710	/// Response to [`SetWebhook`](SetWebhookRequest) request.
711	SetWebhook(SetWebhookResponse),
712	/// Error response to [`SetWebhook`](SetWebhookRequest) request.
713	SetWebhookError(LSPSResponseError),
714	/// Response to [`ListWebhooks`](ListWebhooksRequest) request.
715	ListWebhooks(ListWebhooksResponse),
716	/// Response to [`RemoveWebhook`](RemoveWebhookRequest) request.
717	RemoveWebhook(RemoveWebhookResponse),
718	/// Error response to [`RemoveWebhook`](RemoveWebhookRequest) request.
719	RemoveWebhookError(LSPSResponseError),
720}
721
722#[derive(Clone, Debug, PartialEq, Eq)]
723/// An LSPS5 protocol message.
724pub enum LSPS5Message {
725	/// A request variant.
726	Request(LSPSRequestId, LSPS5Request),
727	/// A response variant.
728	Response(LSPSRequestId, LSPS5Response),
729}
730
731impl TryFrom<LSPSMessage> for LSPS5Message {
732	type Error = ();
733
734	fn try_from(message: LSPSMessage) -> Result<Self, Self::Error> {
735		match message {
736			LSPSMessage::LSPS5(message) => Ok(message),
737			_ => Err(()),
738		}
739	}
740}
741
742impl From<LSPS5Message> for LSPSMessage {
743	fn from(message: LSPS5Message) -> Self {
744		LSPSMessage::LSPS5(message)
745	}
746}
747
748#[cfg(test)]
749mod tests {
750	use super::*;
751	use crate::alloc::string::ToString;
752
753	#[test]
754	fn webhook_notification_serialization() {
755		let notification = WebhookNotification::webhook_registered();
756		let json_str = r#"{"jsonrpc":"2.0","method":"lsps5.webhook_registered","params":{}}"#;
757		assert_eq!(json_str, serde_json::json!(notification).to_string());
758
759		let notification = WebhookNotification::expiry_soon(144);
760		let json_str = r#"{"jsonrpc":"2.0","method":"lsps5.expiry_soon","params":{"timeout":144}}"#;
761		assert_eq!(json_str, serde_json::json!(notification).to_string());
762	}
763
764	#[test]
765	fn parse_set_webhook_request() {
766		let json_str = r#"{"app_name":"my_app","webhook":"https://example.com/webhook"}"#;
767		let request: SetWebhookRequest = serde_json::from_str(json_str).unwrap();
768		assert_eq!(request.app_name, LSPS5AppName::new("my_app".to_string()).unwrap());
769		assert_eq!(
770			request.webhook,
771			LSPS5WebhookUrl::new("https://example.com/webhook".to_string()).unwrap()
772		);
773	}
774
775	#[test]
776	fn parse_set_webhook_response() {
777		let json_str = r#"{"num_webhooks":1,"max_webhooks":5,"no_change":false}"#;
778		let response: SetWebhookResponse = serde_json::from_str(json_str).unwrap();
779		assert_eq!(response.num_webhooks, 1);
780		assert_eq!(response.max_webhooks, 5);
781		assert_eq!(response.no_change, false);
782	}
783
784	#[test]
785	fn parse_list_webhooks_response() {
786		let json_str = r#"{"app_names":["app1","app2"],"max_webhooks":5}"#;
787		let response: ListWebhooksResponse = serde_json::from_str(json_str).unwrap();
788		let app1 = LSPS5AppName::new("app1".to_string()).unwrap();
789		let app2 = LSPS5AppName::new("app2".to_string()).unwrap();
790		assert_eq!(response.app_names, vec![app1, app2]);
791		assert_eq!(response.max_webhooks, 5);
792	}
793
794	#[test]
795	fn parse_empty_requests_responses() {
796		let json_str = r#"{}"#;
797		let _list_req: ListWebhooksRequest = serde_json::from_str(json_str).unwrap();
798		let _remove_resp: RemoveWebhookResponse = serde_json::from_str(json_str).unwrap();
799	}
800
801	#[test]
802	fn spec_example_set_webhook_request() {
803		let json_str = r#"{"app_name":"My LSPS-Compliant Lightning Client","webhook":"https://www.example.org/push?l=1234567890abcdefghijklmnopqrstuv&c=best"}"#;
804		let request: SetWebhookRequest = serde_json::from_str(json_str).unwrap();
805		assert_eq!(
806			request.app_name,
807			LSPS5AppName::new("My LSPS-Compliant Lightning Client".to_string()).unwrap()
808		);
809		assert_eq!(
810			request.webhook,
811			LSPS5WebhookUrl::new(
812				"https://www.example.org/push?l=1234567890abcdefghijklmnopqrstuv&c=best"
813					.to_string()
814			)
815			.unwrap()
816		);
817	}
818
819	#[test]
820	fn spec_example_set_webhook_response() {
821		let json_str = r#"{"num_webhooks":2,"max_webhooks":4,"no_change":false}"#;
822		let response: SetWebhookResponse = serde_json::from_str(json_str).unwrap();
823		assert_eq!(response.num_webhooks, 2);
824		assert_eq!(response.max_webhooks, 4);
825		assert_eq!(response.no_change, false);
826	}
827
828	#[test]
829	fn spec_example_list_webhooks_response() {
830		let json_str = r#"{"app_names":["My LSPS-Compliant Lightning Wallet","Another Wallet With The Same Signing Device"],"max_webhooks":42}"#;
831		let response: ListWebhooksResponse = serde_json::from_str(json_str).unwrap();
832		let app1 = LSPS5AppName::new("My LSPS-Compliant Lightning Wallet".to_string()).unwrap();
833		let app2 =
834			LSPS5AppName::new("Another Wallet With The Same Signing Device".to_string()).unwrap();
835		assert_eq!(response.app_names, vec![app1, app2]);
836		assert_eq!(response.max_webhooks, 42);
837	}
838
839	#[test]
840	fn spec_example_remove_webhook_request() {
841		let json_str = r#"{"app_name":"Another Wallet With The Same Signig Device"}"#;
842		let request: RemoveWebhookRequest = serde_json::from_str(json_str).unwrap();
843		assert_eq!(
844			request.app_name,
845			LSPS5AppName::new("Another Wallet With The Same Signig Device".to_string()).unwrap()
846		);
847	}
848
849	#[test]
850	fn spec_example_webhook_notifications() {
851		let json_str = r#"{"jsonrpc":"2.0","method":"lsps5.webhook_registered","params":{}}"#;
852		let notification: WebhookNotification = serde_json::from_str(json_str).unwrap();
853		assert_eq!(notification.method, WebhookNotificationMethod::LSPS5WebhookRegistered);
854
855		let notification = WebhookNotification::payment_incoming();
856		let json_str = r#"{"jsonrpc":"2.0","method":"lsps5.payment_incoming","params":{}}"#;
857		assert_eq!(json_str, serde_json::json!(notification).to_string());
858
859		let notification = WebhookNotification::expiry_soon(144);
860		let json_str = r#"{"jsonrpc":"2.0","method":"lsps5.expiry_soon","params":{"timeout":144}}"#;
861		assert_eq!(json_str, serde_json::json!(notification).to_string());
862
863		let notification = WebhookNotification::liquidity_management_request();
864		let json_str =
865			r#"{"jsonrpc":"2.0","method":"lsps5.liquidity_management_request","params":{}}"#;
866		assert_eq!(json_str, serde_json::json!(notification).to_string());
867
868		let notification = WebhookNotification::onion_message_incoming();
869		let json_str = r#"{"jsonrpc":"2.0","method":"lsps5.onion_message_incoming","params":{}}"#;
870		assert_eq!(json_str, serde_json::json!(notification).to_string());
871	}
872
873	#[test]
874	fn test_url_security_validation() {
875		let urls_that_should_throw = [
876			"test-app",
877			"http://example.com/webhook",
878			"ftp://example.com/webhook",
879			"ws://example.com/webhook",
880			"ws+unix://example.com/webhook",
881			"ws+unix:/example.com/webhook",
882			"ws+unix://example.com/webhook?param=value",
883			"ws+unix:/example.com/webhook?param=value",
884		];
885
886		for url_str in urls_that_should_throw.iter() {
887			match LSPS5WebhookUrl::new(url_str.to_string()) {
888				Ok(_) => panic!("Expected error"),
889				Err(e) => {
890					let protocol_error = match e {
891						LSPS5Error::Protocol(err) => err,
892						_ => panic!("Expected protocol error"),
893					};
894					let code = protocol_error.code();
895					assert!(
896						code == LSPS5_UNSUPPORTED_PROTOCOL_ERROR_CODE
897							|| code == LSPS5_URL_PARSE_ERROR_CODE
898					);
899				},
900			}
901		}
902	}
903
904	#[test]
905	fn test_webhook_notification_parameter_binding() {
906		let notification = WebhookNotification::expiry_soon(144);
907		if let WebhookNotificationMethod::LSPS5ExpirySoon { timeout } = notification.method {
908			assert_eq!(timeout, 144);
909		} else {
910			panic!("Expected LSPS5ExpirySoon variant");
911		}
912
913		let json = serde_json::to_string(&notification).unwrap();
914		assert_eq!(
915			json,
916			r#"{"jsonrpc":"2.0","method":"lsps5.expiry_soon","params":{"timeout":144}}"#
917		);
918		let deserialized: WebhookNotification = serde_json::from_str(&json).unwrap();
919		if let WebhookNotificationMethod::LSPS5ExpirySoon { timeout } = deserialized.method {
920			assert_eq!(timeout, 144);
921		} else {
922			panic!("Expected LSPS5ExpirySoon variant after deserialization");
923		}
924	}
925
926	#[test]
927	fn test_missing_parameter_error() {
928		let json_without_timeout = r#"{"jsonrpc":"2.0","method":"lsps5.expiry_soon","params":{}}"#;
929
930		let result: Result<WebhookNotification, _> = serde_json::from_str(json_without_timeout);
931		assert!(result.is_err(), "Should fail when timeout parameter is missing");
932
933		let err = result.unwrap_err().to_string();
934		assert!(
935			err.contains("Missing or invalid timeout parameter"),
936			"Error should mention missing parameter: {}",
937			err
938		);
939	}
940
941	#[test]
942	fn test_notification_round_trip_all_types() {
943		let notifications = vec![
944			WebhookNotification::webhook_registered(),
945			WebhookNotification::payment_incoming(),
946			WebhookNotification::expiry_soon(123),
947			WebhookNotification::liquidity_management_request(),
948			WebhookNotification::onion_message_incoming(),
949		];
950
951		for original in notifications {
952			let json = serde_json::to_string(&original).unwrap();
953			let deserialized: WebhookNotification = serde_json::from_str(&json).unwrap();
954
955			assert_eq!(original, deserialized);
956
957			if let WebhookNotificationMethod::LSPS5ExpirySoon { timeout: original_timeout } =
958				original.method
959			{
960				if let WebhookNotificationMethod::LSPS5ExpirySoon {
961					timeout: deserialized_timeout,
962				} = deserialized.method
963				{
964					assert_eq!(original_timeout, deserialized_timeout);
965				} else {
966					panic!("Expected LSPS5ExpirySoon after deserialization");
967				}
968			}
969		}
970	}
971
972	#[test]
973	fn test_all_notification_methods_from_spec() {
974		let methods = [
975			("lsps5.webhook_registered", WebhookNotificationMethod::LSPS5WebhookRegistered, "{}"),
976			("lsps5.payment_incoming", WebhookNotificationMethod::LSPS5PaymentIncoming, "{}"),
977			(
978				"lsps5.expiry_soon",
979				WebhookNotificationMethod::LSPS5ExpirySoon { timeout: 144 },
980				"{\"timeout\":144}",
981			),
982			(
983				"lsps5.liquidity_management_request",
984				WebhookNotificationMethod::LSPS5LiquidityManagementRequest,
985				"{}",
986			),
987			(
988				"lsps5.onion_message_incoming",
989				WebhookNotificationMethod::LSPS5OnionMessageIncoming,
990				"{}",
991			),
992		];
993
994		for (method_name, method_enum, params_json) in methods {
995			let json = format!(
996				r#"{{"jsonrpc":"2.0","method":"{}","params":{}}}"#,
997				method_name, params_json
998			);
999
1000			let notification: WebhookNotification = serde_json::from_str(&json).unwrap();
1001
1002			assert_eq!(notification.method, method_enum);
1003
1004			let serialized = serde_json::to_string(&notification).unwrap();
1005			assert!(serialized.contains(&format!("\"method\":\"{}\"", method_name)));
1006
1007			if method_name == "lsps5.expiry_soon" {
1008				assert!(serialized.contains("\"timeout\":144"));
1009			}
1010		}
1011	}
1012}