1use 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
37pub const MAX_APP_NAME_LENGTH: usize = 64;
39
40pub const MAX_WEBHOOK_URL_LENGTH: usize = 1024;
42
43pub const LSPS5_TOO_LONG_ERROR_CODE: i32 = 500;
45pub const LSPS5_URL_PARSE_ERROR_CODE: i32 = 501;
47pub const LSPS5_UNSUPPORTED_PROTOCOL_ERROR_CODE: i32 = 502;
49pub const LSPS5_TOO_MANY_WEBHOOKS_ERROR_CODE: i32 = 503;
51pub const LSPS5_APP_NAME_NOT_FOUND_ERROR_CODE: i32 = 1010;
53pub const LSPS5_UNKNOWN_ERROR_CODE: i32 = 1000;
55pub const LSPS5_SERIALIZATION_ERROR_CODE: i32 = 1001;
57pub const LSPS5_SLOW_DOWN_ERROR_CODE: i32 = 1002;
59pub 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#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
79pub enum LSPS5ProtocolError {
80 AppNameTooLong,
84
85 WebhookUrlTooLong,
89
90 UrlParse,
94
95 UnsupportedProtocol,
99
100 TooManyWebhooks,
102
103 AppNameNotFound,
107
108 UnknownError,
110
111 SerializationError,
113
114 SlowDownError,
121
122 NoPriorActivityError,
125}
126
127impl LSPS5ProtocolError {
128 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 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#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
183pub enum LSPS5ClientError {
184 InvalidSignature,
188
189 ReplayAttack,
194
195 SerializationError,
197}
198
199impl LSPS5ClientError {
200 const BASE: i32 = 100_000;
201 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 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)]
222pub enum LSPS5Error {
228 Protocol(LSPS5ProtocolError),
232
233 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#[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 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 pub fn from_string(app_name: String) -> Result<Self, LSPS5Error> {
319 Self::new(app_name)
320 }
321
322 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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
378pub struct LSPS5WebhookUrl(LSPSUrl);
379
380impl LSPS5WebhookUrl {
381 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 pub fn from_string(url: String) -> Result<Self, LSPS5Error> {
393 Self::new(url)
394 }
395
396 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) }
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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
466pub struct SetWebhookRequest {
467 pub app_name: LSPS5AppName,
469 pub webhook: LSPS5WebhookUrl,
471}
472
473#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
475pub struct SetWebhookResponse {
476 pub num_webhooks: u32,
478 pub max_webhooks: u32,
480 pub no_change: bool,
482}
483
484#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
486pub struct ListWebhooksRequest {}
487
488#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
490pub struct ListWebhooksResponse {
491 pub app_names: Vec<LSPS5AppName>,
493 pub max_webhooks: u32,
495}
496
497#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
499pub struct RemoveWebhookRequest {
500 pub app_name: LSPS5AppName,
502}
503
504#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
506pub struct RemoveWebhookResponse {}
507
508#[derive(Debug, Clone, PartialEq, Eq, Hash)]
510pub enum WebhookNotificationMethod {
511 LSPS5WebhookRegistered,
513 LSPS5PaymentIncoming,
515 LSPS5ExpirySoon {
517 timeout: u32,
519 },
520 LSPS5LiquidityManagementRequest,
522 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#[derive(Debug, Clone, PartialEq, Eq)]
538pub struct WebhookNotification {
539 pub method: WebhookNotificationMethod,
541}
542
543impl WebhookNotification {
544 pub fn new(method: WebhookNotificationMethod) -> Self {
546 Self { method }
547 }
548
549 pub fn webhook_registered() -> Self {
551 Self::new(WebhookNotificationMethod::LSPS5WebhookRegistered)
552 }
553
554 pub fn payment_incoming() -> Self {
556 Self::new(WebhookNotificationMethod::LSPS5PaymentIncoming)
557 }
558
559 pub fn expiry_soon(timeout: u32) -> Self {
561 Self::new(WebhookNotificationMethod::LSPS5ExpirySoon { timeout })
562 }
563
564 pub fn liquidity_management_request() -> Self {
566 Self::new(WebhookNotificationMethod::LSPS5LiquidityManagementRequest)
567 }
568
569 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", ¶ms)?;
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#[derive(Clone, Debug, PartialEq, Eq)]
692pub enum LSPS5Request {
693 SetWebhook(SetWebhookRequest),
695 ListWebhooks(ListWebhooksRequest),
697 RemoveWebhook(RemoveWebhookRequest),
699}
700
701impl LSPS5Request {
702 pub(crate) fn is_state_allocating(&self) -> bool {
703 matches!(self, LSPS5Request::SetWebhook(_))
704 }
705}
706
707#[derive(Clone, Debug, PartialEq, Eq)]
709pub enum LSPS5Response {
710 SetWebhook(SetWebhookResponse),
712 SetWebhookError(LSPSResponseError),
714 ListWebhooks(ListWebhooksResponse),
716 RemoveWebhook(RemoveWebhookResponse),
718 RemoveWebhookError(LSPSResponseError),
720}
721
722#[derive(Clone, Debug, PartialEq, Eq)]
723pub enum LSPS5Message {
725 Request(LSPSRequestId, LSPS5Request),
727 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(¬ification).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(¬ification).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}