plugins: lsps: move primitives into lsps0

Those primitives are actually defined in lsps0

This commit is part of a series that refactor the lsp plugin crate into
a modularized crate with the goal to separate the actual plugin runtime
as much as possible from library code to make it accessible for 3rd
party plugin implementations.

Signed-off-by: Peter Neuroth <pet.v.ne@gmail.com>
This commit is contained in:
Peter Neuroth
2025-11-26 13:59:10 +01:00
committed by madelinevibes
parent 5847cd443f
commit ba93a7ca62
6 changed files with 203 additions and 206 deletions

View File

@@ -15,9 +15,8 @@ use cln_lsps::lsps2::model::{
Lsps2GetInfoResponse, OpeningFeeParams,
};
use cln_lsps::proto::lsps0::{
Lsps0listProtocolsRequest, Lsps0listProtocolsResponse, LSP_FEATURE_BIT,
Lsps0listProtocolsRequest, Lsps0listProtocolsResponse, Msat, LSP_FEATURE_BIT,
};
use cln_lsps::proto::primitives::Msat;
use cln_lsps::util;
use cln_plugin::options;
use cln_rpc::model::requests::{

View File

@@ -14,7 +14,7 @@ use crate::{
},
proto::{
jsonrpc::{JsonRpcResponse as _, RequestObject, RpcError},
primitives::{Msat, ShortChannelId},
lsps0::{Msat, ShortChannelId},
},
util::unwrap_payload_with_peer_id,
};
@@ -686,7 +686,7 @@ mod tests {
model::PolicyOpeningFeeParams,
},
proto::jsonrpc::{JsonRpcRequest, ResponseObject},
proto::primitives::Ppm,
proto::lsps0::Ppm,
util::wrap_payload_with_peer_id,
};
use chrono::{TimeZone, Utc};

View File

@@ -1,6 +1,6 @@
use crate::{
proto::jsonrpc::{JsonRpcRequest, RpcError},
proto::primitives::{DateTime, Msat, Ppm, ShortChannelId},
proto::lsps0::{DateTime, Msat, Ppm, ShortChannelId},
};
use bitcoin::hashes::{sha256, Hash, HashEngine, Hmac, HmacEngine};
use chrono::Utc;

View File

@@ -1,5 +1,11 @@
use crate::proto::jsonrpc::JsonRpcRequest;
use serde::{Deserialize, Serialize};
use core::fmt;
use serde::{
de::{self},
Deserialize, Deserializer, Serialize, Serializer,
};
const MSAT_PER_SAT: u64 = 1_000;
// Optional feature bet to set according to LSPS0.
pub const LSP_FEATURE_BIT: usize = 729;
@@ -20,6 +26,139 @@ impl Error {
}
}
/// 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 (spec definition)
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>;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Lsps0listProtocolsRequest {}
@@ -31,3 +170,62 @@ impl JsonRpcRequest for Lsps0listProtocolsRequest {
pub struct Lsps0listProtocolsResponse {
pub protocols: Vec<u8>,
}
#[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"
);
}
}

View File

@@ -1,3 +1,2 @@
pub mod jsonrpc;
pub mod lsps0;
pub mod primitives;

View File

@@ -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"
);
}
}