lsp_plugin: add primitives for messages
Adds some primitives defined in lsps0 for other protocol messages. Signed-off-by: Peter Neuroth <pet.v.ne@gmail.com>
This commit is contained in:
committed by
Rusty Russell
parent
3606106c4f
commit
e36fdeff81
106
Cargo.lock
generated
106
Cargo.lock
generated
@@ -26,6 +26,15 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.98"
|
||||
@@ -464,6 +473,19 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cln-bip353"
|
||||
version = "0.1.0"
|
||||
@@ -524,6 +546,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"cln-plugin",
|
||||
"cln-rpc",
|
||||
"hex",
|
||||
@@ -1209,6 +1232,30 @@ dependencies = [
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "1.5.0"
|
||||
@@ -3336,6 +3383,65 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
|
||||
@@ -14,6 +14,7 @@ path = "src/service.rs"
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
async-trait = "0.1"
|
||||
chrono = "0.4.42"
|
||||
cln-plugin = { version = "0.5", path = "../" }
|
||||
cln-rpc = { version = "0.5", path = "../../cln-rpc" }
|
||||
hex = "0.4"
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod model;
|
||||
pub mod primitives;
|
||||
pub mod transport;
|
||||
|
||||
199
plugins/lsps-plugin/src/lsps0/primitives.rs
Normal file
199
plugins/lsps-plugin/src/lsps0/primitives.rs
Normal file
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user