plugins: lsps: move primitives into proto module
What changed, and why it matters
This commit is a straightforward code reorganization. It moves shared data types (like monetary amounts and channel identifiers) from one internal module location to another, without changing how they work. There is no security-relevant change visible in the diff.
No security action required. Treat as normal code maintenance.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit refactors the lsps-plugin crate by moving primitives.rs from lsps0::primitives to a new proto::primitives module. All imports in client.rs, lsps2/handler.rs, and lsps2/model.rs are updated accordingly. The implementation of Msat, Ppm, ShortChannelId, and DateTime remains byte-for-byte identical, including the unit tests. This is a pure structural refactor with no functional or security changes.
Changed components
plugins/lsps-plugin/src/lsps0/primitives.rs (deleted)plugins/lsps-plugin/src/proto/primitives.rs (created)plugins/lsps-plugin/src/proto/mod.rs (created)plugins/lsps-plugin/src/client.rs (import update)plugins/lsps-plugin/src/lsps2/handler.rs (import update)plugins/lsps-plugin/src/lsps2/model.rs (import update)plugins/lsps-plugin/src/lib.rs (module declaration update)Inspect captured patch +205 / −203
diff --git a/plugins/lsps-plugin/src/client.rs b/plugins/lsps-plugin/src/client.rs
index a466560f..88189c64 100644
--- a/plugins/lsps-plugin/src/client.rs
+++ b/plugins/lsps-plugin/src/client.rs
@@ -16,6 +16,7 @@ use cln_lsps::lsps2::model::{
compute_opening_fee, Lsps2BuyRequest, Lsps2BuyResponse, Lsps2GetInfoRequest,
Lsps2GetInfoResponse, OpeningFeeParams,
};
+use cln_lsps::proto::primitives::Msat;
use cln_lsps::util;
use cln_lsps::LSP_FEATURE_BIT;
use cln_plugin::options;
diff --git a/plugins/lsps-plugin/src/lib.rs b/plugins/lsps-plugin/src/lib.rs
index f14b96c7..d4ef4ae7 100644
--- a/plugins/lsps-plugin/src/lib.rs
+++ b/plugins/lsps-plugin/src/lib.rs
@@ -1,6 +1,7 @@
pub mod jsonrpc;
pub mod lsps0;
pub mod lsps2;
+pub mod proto;
pub mod util;
pub const LSP_FEATURE_BIT: usize = 729;
diff --git a/plugins/lsps-plugin/src/lsps0/mod.rs b/plugins/lsps-plugin/src/lsps0/mod.rs
index f32b0a55..d4959a32 100644
--- a/plugins/lsps-plugin/src/lsps0/mod.rs
+++ b/plugins/lsps-plugin/src/lsps0/mod.rs
@@ -1,4 +1,3 @@
pub mod handler;
pub mod model;
-pub mod primitives;
pub mod transport;
diff --git a/plugins/lsps-plugin/src/lsps0/primitives.rs b/plugins/lsps-plugin/src/lsps0/primitives.rs
deleted file mode 100644
index 3b2ff52d..00000000
--- a/plugins/lsps-plugin/src/lsps0/primitives.rs
+++ /dev/null
@@ -1,199 +0,0 @@
-use core::fmt;
-use serde::{
- de::{self},
- Deserialize, Deserializer, Serialize, Serializer,
-};
-
-const MSAT_PER_SAT: u64 = 1_000;
-
-/// Represents a monetary amount as defined in LSPS0.msat. Is converted to a
-/// `String` in json messages with a suffix `_msat` or `_sat` and internally
-/// represented as Millisatoshi `u64`.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
-pub struct Msat(pub u64);
-
-impl Msat {
- /// Constructs a new `Msat` struct from a `u64` msat value.
- pub fn from_msat(msat: u64) -> Self {
- Msat(msat)
- }
-
- /// Construct a new `Msat` struct from a `u64` sat value.
- pub fn from_sat(sat: u64) -> Self {
- Msat(sat * MSAT_PER_SAT)
- }
-
- /// Returns the sat amount of the field. Is a floored integer division e.g
- /// 100678 becomes 100.
- pub fn to_sats_floor(&self) -> u64 {
- self.0 / 1000
- }
-
- /// Returns the msat value as `u64`. Is the inner value of `Msat`.
- pub fn msat(&self) -> u64 {
- self.0
- }
-}
-
-impl core::fmt::Display for Msat {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}_msat", self.0)
- }
-}
-
-impl Serialize for Msat {
- fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
- where
- S: Serializer,
- {
- serializer.serialize_str(&self.0.to_string())
- }
-}
-
-impl<'de> Deserialize<'de> for Msat {
- fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
- where
- D: Deserializer<'de>,
- {
- struct MsatVisitor;
-
- impl<'de> de::Visitor<'de> for MsatVisitor {
- type Value = Msat;
-
- fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
- formatter.write_str("a string representing a number")
- }
-
- fn visit_str<E>(self, value: &str) -> Result<Msat, E>
- where
- E: de::Error,
- {
- value
- .parse::<u64>()
- .map(Msat::from_msat)
- .map_err(|_| E::custom(format!("Invalid number string: {}", value)))
- }
-
- // Also handle if JSON mistakenly has a number instead of string
- fn visit_u64<E>(self, value: u64) -> Result<Msat, E>
- where
- E: de::Error,
- {
- Ok(Msat::from_msat(value))
- }
-
- fn visit_i64<E>(self, value: i64) -> Result<Msat, E>
- where
- E: de::Error,
- {
- if value < 0 {
- Err(E::custom("Msat cannot be negative"))
- } else {
- Ok(Msat::from_msat(value as u64))
- }
- }
- }
-
- deserializer.deserialize_any(MsatVisitor)
- }
-}
-
-/// Represents parts-per-million as defined in LSPS0.ppm. Gets it's own type
-/// from the rationals: "This is its own type so that fractions can be expressed
-/// using this type, instead of as a floating-point type which might lose
-/// accuracy when serialized into text.". Having it as a separate type also
-/// provides more clarity.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
-#[serde(transparent)] // Key attribute! Serialize/Deserialize as the inner u32
-pub struct Ppm(pub u32); // u32 is sufficient as 1,000,000 fits easily
-
-impl Ppm {
- /// Constructs a new `Ppm` from a u32.
- pub const fn from_ppm(value: u32) -> Self {
- Ppm(value)
- }
-
- /// Applies the proportion to a base amount (e.g., in msats).
- pub fn apply_to(&self, base_msat: u64) -> u64 {
- // Careful about integer division order and potential overflow
- (base_msat as u128 * self.0 as u128 / 1_000_000) as u64
- }
-
- /// Returns the ppm.
- pub fn ppm(&self) -> u32 {
- self.0
- }
-}
-
-impl core::fmt::Display for Ppm {
- fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
- write!(f, "{}ppm", self.0)
- }
-}
-
-/// Represents a short channel id as defined in LSPS0.scid. Matches with the
-/// implementation in cln_rpc.
-pub type ShortChannelId = cln_rpc::primitives::ShortChannelId;
-
-/// Represents a datetime as defined in LSPS0.datetime. Uses ISO8601 in UTC
-/// timezone.
-pub type DateTime = chrono::DateTime<chrono::Utc>;
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use serde_json;
-
- #[derive(Debug, Serialize, Deserialize)]
- struct TestMessage {
- amount: Msat,
- }
-
- /// Test serialization of a struct containing Msat.
- #[test]
- fn test_msat_serialization() {
- let msg = TestMessage {
- amount: Msat(12345000),
- };
-
- let expected_amount_json = r#""amount":"12345000""#;
-
- // Assert that the field gets serialized as string.
- let json_string = serde_json::to_string(&msg).expect("Serialization failed");
- assert!(
- json_string.contains(expected_amount_json),
- "Serialized JSON should contain '{}'",
- expected_amount_json
- );
-
- // Parse back to generic json value and check field.
- let json_value: serde_json::Value =
- serde_json::from_str(&json_string).expect("Failed to parse JSON back");
- assert_eq!(
- json_value
- .get("amount")
- .expect("JSON should have 'amount' field"),
- &serde_json::Value::String("12345000".to_string()),
- "JSON 'amount' field should have the correct string value"
- );
- }
-
- /// Test deserialization into a struct containing Msat.
- #[test]
- fn test_msat_deserialization_and_errors() {
- // Case 1: Input string uses "_msat" suffix
- let json_ok = r#"{"amount":"987654321"}"#;
- let expected_value_msat = Msat(987654321);
- let message1: TestMessage =
- serde_json::from_str(json_ok).expect("Deserialization from string failed");
- assert_eq!(message1.amount, expected_value_msat);
-
- // Case 2: Non-numeric Value before suffix
- let json_non_numeric = r#"{"amount":"abc"}"#;
- let result_non_numeric = serde_json::from_str::<TestMessage>(json_non_numeric);
- assert!(
- result_non_numeric.is_err(),
- "Deserialization should fail for non-numeric value"
- );
- }
-}
diff --git a/plugins/lsps-plugin/src/lsps2/handler.rs b/plugins/lsps-plugin/src/lsps2/handler.rs
index 4e86943f..1eb323fe 100644
--- a/plugins/lsps-plugin/src/lsps2/handler.rs
+++ b/plugins/lsps-plugin/src/lsps2/handler.rs
@@ -1,6 +1,5 @@
use crate::{
jsonrpc::{server::RequestHandler, JsonRpcResponse as _, RequestObject, RpcError},
- lsps0::primitives::{Msat, ShortChannelId},
lsps2::{
cln::{HtlcAcceptedRequest, HtlcAcceptedResponse, TLV_FORWARD_AMT},
model::{
@@ -13,6 +12,7 @@ use crate::{
},
DS_MAIN_KEY, DS_SUB_KEY,
},
+ proto::primitives::{Msat, ShortChannelId},
util::unwrap_payload_with_peer_id,
};
use anyhow::{Context, Result as AnyResult};
@@ -679,11 +679,11 @@ mod tests {
use super::*;
use crate::{
jsonrpc::{JsonRpcRequest, ResponseObject},
- lsps0::primitives::{Msat, Ppm},
lsps2::{
cln::{tlv::TlvStream, HtlcAcceptedResult},
model::PolicyOpeningFeeParams,
},
+ proto::primitives::Ppm,
util::wrap_payload_with_peer_id,
};
use chrono::{TimeZone, Utc};
diff --git a/plugins/lsps-plugin/src/lsps2/model.rs b/plugins/lsps-plugin/src/lsps2/model.rs
index bcf8de07..c9767dee 100644
--- a/plugins/lsps-plugin/src/lsps2/model.rs
+++ b/plugins/lsps-plugin/src/lsps2/model.rs
@@ -1,6 +1,6 @@
use crate::{
jsonrpc::{JsonRpcRequest, RpcError},
- lsps0::primitives::{DateTime, Msat, Ppm, ShortChannelId},
+ proto::primitives::{DateTime, Msat, Ppm, ShortChannelId},
};
use bitcoin::hashes::{sha256, Hash, HashEngine, Hmac, HmacEngine};
use chrono::Utc;
diff --git a/plugins/lsps-plugin/src/proto/mod.rs b/plugins/lsps-plugin/src/proto/mod.rs
new file mode 100644
index 00000000..23cd13c5
--- /dev/null
+++ b/plugins/lsps-plugin/src/proto/mod.rs
@@ -0,0 +1 @@
+pub mod primitives;
diff --git a/plugins/lsps-plugin/src/proto/primitives.rs b/plugins/lsps-plugin/src/proto/primitives.rs
new file mode 100644
index 00000000..3b2ff52d
--- /dev/null
+++ b/plugins/lsps-plugin/src/proto/primitives.rs
@@ -0,0 +1,199 @@
+use core::fmt;
+use serde::{
+ de::{self},
+ Deserialize, Deserializer, Serialize, Serializer,
+};
+
+const MSAT_PER_SAT: u64 = 1_000;
+
+/// Represents a monetary amount as defined in LSPS0.msat. Is converted to a
+/// `String` in json messages with a suffix `_msat` or `_sat` and internally
+/// represented as Millisatoshi `u64`.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+pub struct Msat(pub u64);
+
+impl Msat {
+ /// Constructs a new `Msat` struct from a `u64` msat value.
+ pub fn from_msat(msat: u64) -> Self {
+ Msat(msat)
+ }
+
+ /// Construct a new `Msat` struct from a `u64` sat value.
+ pub fn from_sat(sat: u64) -> Self {
+ Msat(sat * MSAT_PER_SAT)
+ }
+
+ /// Returns the sat amount of the field. Is a floored integer division e.g
+ /// 100678 becomes 100.
+ pub fn to_sats_floor(&self) -> u64 {
+ self.0 / 1000
+ }
+
+ /// Returns the msat value as `u64`. Is the inner value of `Msat`.
+ pub fn msat(&self) -> u64 {
+ self.0
+ }
+}
+
+impl core::fmt::Display for Msat {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}_msat", self.0)
+ }
+}
+
+impl Serialize for Msat {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.serialize_str(&self.0.to_string())
+ }
+}
+
+impl<'de> Deserialize<'de> for Msat {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct MsatVisitor;
+
+ impl<'de> de::Visitor<'de> for MsatVisitor {
+ type Value = Msat;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a string representing a number")
+ }
+
+ fn visit_str<E>(self, value: &str) -> Result<Msat, E>
+ where
+ E: de::Error,
+ {
+ value
+ .parse::<u64>()
+ .map(Msat::from_msat)
+ .map_err(|_| E::custom(format!("Invalid number string: {}", value)))
+ }
+
+ // Also handle if JSON mistakenly has a number instead of string
+ fn visit_u64<E>(self, value: u64) -> Result<Msat, E>
+ where
+ E: de::Error,
+ {
+ Ok(Msat::from_msat(value))
+ }
+
+ fn visit_i64<E>(self, value: i64) -> Result<Msat, E>
+ where
+ E: de::Error,
+ {
+ if value < 0 {
+ Err(E::custom("Msat cannot be negative"))
+ } else {
+ Ok(Msat::from_msat(value as u64))
+ }
+ }
+ }
+
+ deserializer.deserialize_any(MsatVisitor)
+ }
+}
+
+/// Represents parts-per-million as defined in LSPS0.ppm. Gets it's own type
+/// from the rationals: "This is its own type so that fractions can be expressed
+/// using this type, instead of as a floating-point type which might lose
+/// accuracy when serialized into text.". Having it as a separate type also
+/// provides more clarity.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
+#[serde(transparent)] // Key attribute! Serialize/Deserialize as the inner u32
+pub struct Ppm(pub u32); // u32 is sufficient as 1,000,000 fits easily
+
+impl Ppm {
+ /// Constructs a new `Ppm` from a u32.
+ pub const fn from_ppm(value: u32) -> Self {
+ Ppm(value)
+ }
+
+ /// Applies the proportion to a base amount (e.g., in msats).
+ pub fn apply_to(&self, base_msat: u64) -> u64 {
+ // Careful about integer division order and potential overflow
+ (base_msat as u128 * self.0 as u128 / 1_000_000) as u64
+ }
+
+ /// Returns the ppm.
+ pub fn ppm(&self) -> u32 {
+ self.0
+ }
+}
+
+impl core::fmt::Display for Ppm {
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ write!(f, "{}ppm", self.0)
+ }
+}
+
+/// Represents a short channel id as defined in LSPS0.scid. Matches with the
+/// implementation in cln_rpc.
+pub type ShortChannelId = cln_rpc::primitives::ShortChannelId;
+
+/// Represents a datetime as defined in LSPS0.datetime. Uses ISO8601 in UTC
+/// timezone.
+pub type DateTime = chrono::DateTime<chrono::Utc>;
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use serde_json;
+
+ #[derive(Debug, Serialize, Deserialize)]
+ struct TestMessage {
+ amount: Msat,
+ }
+
+ /// Test serialization of a struct containing Msat.
+ #[test]
+ fn test_msat_serialization() {
+ let msg = TestMessage {
+ amount: Msat(12345000),
+ };
+
+ let expected_amount_json = r#""amount":"12345000""#;
+
+ // Assert that the field gets serialized as string.
+ let json_string = serde_json::to_string(&msg).expect("Serialization failed");
+ assert!(
+ json_string.contains(expected_amount_json),
+ "Serialized JSON should contain '{}'",
+ expected_amount_json
+ );
+
+ // Parse back to generic json value and check field.
+ let json_value: serde_json::Value =
+ serde_json::from_str(&json_string).expect("Failed to parse JSON back");
+ assert_eq!(
+ json_value
+ .get("amount")
+ .expect("JSON should have 'amount' field"),
+ &serde_json::Value::String("12345000".to_string()),
+ "JSON 'amount' field should have the correct string value"
+ );
+ }
+
+ /// Test deserialization into a struct containing Msat.
+ #[test]
+ fn test_msat_deserialization_and_errors() {
+ // Case 1: Input string uses "_msat" suffix
+ let json_ok = r#"{"amount":"987654321"}"#;
+ let expected_value_msat = Msat(987654321);
+ let message1: TestMessage =
+ serde_json::from_str(json_ok).expect("Deserialization from string failed");
+ assert_eq!(message1.amount, expected_value_msat);
+
+ // Case 2: Non-numeric Value before suffix
+ let json_non_numeric = r#"{"amount":"abc"}"#;
+ let result_non_numeric = serde_json::from_str::<TestMessage>(json_non_numeric);
+ assert!(
+ result_non_numeric.is_err(),
+ "Deserialization should fail for non-numeric value"
+ );
+ }
+}
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.