From 62f0aa640c2ffb9459af8a0fc41f9e3de1c5472b Mon Sep 17 00:00:00 2001 From: Erik De Smedt Date: Wed, 20 Sep 2023 14:35:23 +0200 Subject: [PATCH] Separate lsps0 schema's The `crate::lsps::message`-module was becoming too big. I've decided to separate all files related to LSPS0. The code should better reflect how the LSP-specification is structured. Follow-up commits for LSPS1 and LSPS2 will follow --- .../src/lsps/lsps0/common_schemas.rs | 147 +++++++++++++++++ libs/gl-client/src/lsps/lsps0/mod.rs | 2 + libs/gl-client/src/lsps/lsps0/schema.rs | 6 + libs/gl-client/src/lsps/message.rs | 153 ++---------------- libs/gl-client/src/lsps/mod.rs | 1 + 5 files changed, 166 insertions(+), 143 deletions(-) create mode 100644 libs/gl-client/src/lsps/lsps0/common_schemas.rs create mode 100644 libs/gl-client/src/lsps/lsps0/mod.rs create mode 100644 libs/gl-client/src/lsps/lsps0/schema.rs diff --git a/libs/gl-client/src/lsps/lsps0/common_schemas.rs b/libs/gl-client/src/lsps/lsps0/common_schemas.rs new file mode 100644 index 000000000..451577516 --- /dev/null +++ b/libs/gl-client/src/lsps/lsps0/common_schemas.rs @@ -0,0 +1,147 @@ +use serde::{Serialize, Deserialize}; +use serde::de::Error as SeError; +use serde::ser::Error as DeError; + +use time::format_description::FormatItem; +use time::macros::format_description; +use time::{OffsetDateTime, PrimitiveDateTime}; + + +// Implements all the common schema's defined in LSPS0 common schema's + +// Initially I used serde_as for the parsing and serialization of this type. +// However, the spec is more strict. +// It requires a yyyy-mm-ddThh:mm:ss.uuuZ format +// +// The serde_as provides us options such as rfc_3339. +// Note, that this also allows formats that are not compliant to the LSP-spec such as dropping +// the fractional seconds or use non UTC timezones. +// +// For LSPS2 the `valid_until`-field must be copied verbatim. As a client this can only be +// achieved if the LSPS2 sends a fully compliant timestamp. +// +// I have decided to fail early if another timestamp is received +#[derive(Debug)] +pub struct IsoDatetime { + pub datetime: PrimitiveDateTime, +} + +const DATETIME_FORMAT: &[FormatItem] = + format_description!("[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:3]Z"); + +impl IsoDatetime { + pub fn from_offset_date_time(datetime: OffsetDateTime) -> Self { + let offset = time::UtcOffset::from_whole_seconds(0).unwrap(); + let datetime_utc = datetime.to_offset(offset); + let primitive = PrimitiveDateTime::new(datetime_utc.date(), datetime.time()); + return Self { + datetime: primitive, + }; + } + + pub fn from_primitive_date_time(datetime: PrimitiveDateTime) -> Self { + return Self { datetime }; + } + + pub fn datetime(&self) -> OffsetDateTime { + return self.datetime.assume_utc(); + } +} + +impl Serialize for IsoDatetime { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let datetime_str = self + .datetime + .format(&DATETIME_FORMAT) + .map_err(|err| S::Error::custom(format!("Failed to format datetime {:?}", err)))?; + + serializer.serialize_str(&datetime_str) + } +} + +impl<'de> Deserialize<'de> for IsoDatetime { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let str_repr = ::deserialize(deserializer)?; + time::PrimitiveDateTime::parse(&str_repr, DATETIME_FORMAT) + .map_err(|err| D::Error::custom(format!("Failed to parse Datetime. {:?}", err))) + .map(|dt| Self::from_primitive_date_time(dt)) + } +} + +#[derive(Debug)] +pub struct SatAmount(u64); +#[derive(Debug)] +pub struct MsatAmount(u64); + +impl SatAmount { + pub fn sat_value(&self) -> u64 { + return self.0; + } + + pub fn new(value : u64) -> Self { + return SatAmount(value) + } +} + +impl MsatAmount { + pub fn msat_value(&self) -> u64 { + return self.0; + } + + + pub fn new(value : u64) -> Self { + return MsatAmount(value) + } +} + +impl Serialize for SatAmount { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let amount_str = self.0.to_string(); + serializer.serialize_str(&amount_str) + } +} + +impl Serialize for MsatAmount { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let amount_str = self.0.to_string(); + serializer.serialize_str(&amount_str) + } +} + +impl<'de> Deserialize<'de> for SatAmount { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let str_repr = ::deserialize(deserializer)?; + let u64_repr: Result = str_repr + .parse() + .map_err(|_| D::Error::custom(String::from("Failed to parse sat_amount"))); + return Ok(Self(u64_repr.unwrap())); + } +} + +impl<'de> Deserialize<'de> for MsatAmount { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let str_repr = ::deserialize(deserializer)?; + let u64_repr: Result = str_repr + .parse() + .map_err(|_| D::Error::custom(String::from("Failed to parse sat_amount"))); + return Ok(Self(u64_repr.unwrap())); + } +} diff --git a/libs/gl-client/src/lsps/lsps0/mod.rs b/libs/gl-client/src/lsps/lsps0/mod.rs new file mode 100644 index 000000000..cd182a592 --- /dev/null +++ b/libs/gl-client/src/lsps/lsps0/mod.rs @@ -0,0 +1,2 @@ +pub mod common_schemas; +pub mod schema; diff --git a/libs/gl-client/src/lsps/lsps0/schema.rs b/libs/gl-client/src/lsps/lsps0/schema.rs new file mode 100644 index 000000000..42993f7bf --- /dev/null +++ b/libs/gl-client/src/lsps/lsps0/schema.rs @@ -0,0 +1,6 @@ +use serde::{Serialize, Deserialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct ProtocolList { + pub protocols: Vec, +} diff --git a/libs/gl-client/src/lsps/message.rs b/libs/gl-client/src/lsps/message.rs index 2d747b12b..905c41452 100644 --- a/libs/gl-client/src/lsps/message.rs +++ b/libs/gl-client/src/lsps/message.rs @@ -2,13 +2,12 @@ use crate::lsps::error::LspsError; pub use crate::lsps::json_rpc::{DefaultError, JsonRpcMethod, MapErrorCode, NoParams}; use crate::lsps::json_rpc_erased::{JsonRpcMethodErased, JsonRpcMethodUnerased}; + +use crate::lsps::lsps0::common_schemas::{IsoDatetime, MsatAmount, SatAmount}; +use crate::lsps::lsps0::schema::{ProtocolList}; use serde::de::Error as DeError; -use serde::ser::Error as SeError; use serde::{Deserialize, Serialize}; -use time::format_description::FormatItem; -use time::macros::format_description; -use time::{OffsetDateTime, PrimitiveDateTime}; use uuid::Uuid; use super::error::map_json_rpc_error_code_to_str; @@ -149,138 +148,6 @@ impl<'a> JsonRpcMethodUnerased<'a, Vec, Vec, Vec> for JsonRpcMethodE } } -#[derive(Debug)] -pub struct SatAmount(u64); -#[derive(Debug)] -pub struct MsatAmount(u64); - -impl SatAmount { - pub fn value(&self) -> u64 { - return self.0; - } -} - -impl MsatAmount { - pub fn value(&self) -> u64 { - return self.0; - } -} - -impl Serialize for SatAmount { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let amount_str = self.0.to_string(); - serializer.serialize_str(&amount_str) - } -} - -impl Serialize for MsatAmount { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let amount_str = self.0.to_string(); - serializer.serialize_str(&amount_str) - } -} - -impl<'de> Deserialize<'de> for SatAmount { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let str_repr = ::deserialize(deserializer)?; - let u64_repr: Result = str_repr - .parse() - .map_err(|_| D::Error::custom(String::from("Failed to parse sat_amount"))); - return Ok(Self(u64_repr.unwrap())); - } -} - -impl<'de> Deserialize<'de> for MsatAmount { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let str_repr = ::deserialize(deserializer)?; - let u64_repr: Result = str_repr - .parse() - .map_err(|_| D::Error::custom(String::from("Failed to parse sat_amount"))); - return Ok(Self(u64_repr.unwrap())); - } -} - -// Initially I used serde_as for the parsing and serialization of this type. -// However, the spec is more strict. -// It requires a yyyy-mm-ddThh:mm:ss.uuuZ format -// -// The serde_as provides us options such as rfc_3339. -// Note, that this also allows formats that are not compliant to the LSP-spec such as dropping -// the fractional seconds or use non UTC timezones. -// -// For LSPS2 the `valid_until`-field must be copied verbatim. As a client this can only be -// achieved if the LSPS2 sends a fully compliant timestamp. -// -// I have decided to fail early if another timestamp is received -#[derive(Debug)] -pub struct Datetime { - datetime: PrimitiveDateTime, -} - -const DATETIME_FORMAT: &[FormatItem] = - format_description!("[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:3]Z"); - -impl Datetime { - pub fn from_offset_date_time(datetime: OffsetDateTime) -> Self { - let offset = time::UtcOffset::from_whole_seconds(0).unwrap(); - let datetime_utc = datetime.to_offset(offset); - let primitive = PrimitiveDateTime::new(datetime_utc.date(), datetime.time()); - return Self { - datetime: primitive, - }; - } - - pub fn from_primitive_date_time(datetime: PrimitiveDateTime) -> Self { - return Self { datetime: datetime }; - } - - pub fn datetime(&self) -> OffsetDateTime { - return self.datetime.assume_utc(); - } -} - -impl Serialize for Datetime { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let datetime_str = self - .datetime - .format(&DATETIME_FORMAT) - .map_err(|err| S::Error::custom(format!("Failed to format datetime {:?}", err)))?; - - serializer.serialize_str(&datetime_str) - } -} - -impl<'de> Deserialize<'de> for Datetime { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let str_repr = ::deserialize(deserializer)?; - time::PrimitiveDateTime::parse(&str_repr, DATETIME_FORMAT) - .map_err(|err| D::Error::custom(format!("Failed to parse Datetime. {:?}", err))) - .map(|dt| Self::from_primitive_date_time(dt)) - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ProtocolList { - pub protocols: Vec, -} #[derive(Debug, Serialize, Deserialize)] pub struct Lsps1InfoResponse { @@ -327,8 +194,8 @@ pub struct Lsps1GetOrderResponse { token: String, #[serde(rename = "announceChannel")] announce_channel: bool, - created_at: Datetime, - expires_at: Datetime, + created_at: IsoDatetime, + expires_at: IsoDatetime, order_state: OrderState, payment: Payment, } @@ -412,7 +279,7 @@ impl MapErrorCode for Lsp2GetInfoError { pub struct OpeningFeeParamsMenuItem { min_fee_msat: MsatAmount, proportional: u64, - _valid_until: Datetime, + _valid_until: IsoDatetime, min_lifetime: u64, max_client_to_self_delay: u64, promise: Promise, @@ -516,7 +383,7 @@ mod test { let int_number: u64 = 10000000001; let x = serde_json::from_str::(json_str_number).unwrap(); - assert_eq!(x.0, int_number); + assert_eq!(x.sat_value(), int_number); } #[test] @@ -526,7 +393,7 @@ mod test { let json_str_number = "\"10000000001\""; let int_number: u64 = 10000000001; - let sat_amount = SatAmount(int_number); + let sat_amount = SatAmount::new(int_number); let json_str = serde_json::to_string::(&sat_amount).unwrap(); assert_eq!(json_str, json_str_number); @@ -536,7 +403,7 @@ mod test { fn parse_and_serialize_datetime() { let datetime_str = "\"2023-01-01T23:59:59.999Z\""; - let dt = serde_json::from_str::(&datetime_str).unwrap(); + let dt = serde_json::from_str::(&datetime_str).unwrap(); assert_eq!(dt.datetime.year(), 2023); assert_eq!(dt.datetime.month(), time::Month::January); @@ -557,7 +424,7 @@ mod test { // However, in LSPS2 the datetime_str must be repeated verbatim let datetime_str = "\"2023-01-01T23:59:59.99Z\""; - let result = serde_json::from_str::(&datetime_str); + let result = serde_json::from_str::(&datetime_str); result.expect_err("datetime_str should not be parsed if it doesn't follow spec"); } diff --git a/libs/gl-client/src/lsps/mod.rs b/libs/gl-client/src/lsps/mod.rs index 7ba871b1b..f945b28cb 100644 --- a/libs/gl-client/src/lsps/mod.rs +++ b/libs/gl-client/src/lsps/mod.rs @@ -4,3 +4,4 @@ pub mod json_rpc; pub mod json_rpc_erased; pub mod message; pub mod transport; +pub mod lsps0;