lexe-common
Advanced tools
| { | ||
| "git": { | ||
| "sha1": "cf3dd7c7a928b81961f3dd0e9b11555db1d0f69b" | ||
| "sha1": "69c38083fdf5e19e1f253911331ec830bc38bee0" | ||
| }, | ||
| "path_in_vcs": "public/lexe-common" | ||
| } |
+22
-43
@@ -38,3 +38,3 @@ # This file is automatically @generated by Cargo. | ||
| "rusticata-macros", | ||
| "thiserror 2.0.17", | ||
| "thiserror", | ||
| "time", | ||
@@ -101,8 +101,7 @@ ] | ||
| name = "bcs" | ||
| version = "0.1.6" | ||
| version = "0.2.1" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "85b6598a2f5d564fb7855dc6b06fd1c38cff5a72bd8b863a4d021938497b440a" | ||
| checksum = "350f2b5fa7b76b498158ec1079dc0ea842c5b622b6b3f675005fddd889f2c9a7" | ||
| dependencies = [ | ||
| "serde", | ||
| "thiserror 1.0.69", | ||
| "serde_core", | ||
| ] | ||
@@ -509,5 +508,5 @@ | ||
| name = "lexe-byte-array" | ||
| version = "0.1.10" | ||
| version = "0.1.11" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "58ca8f808745e4ff3cccb305ba98eed27a87ada44533f43a76ed3fd57e9a4876" | ||
| checksum = "08b55dbb73da593af8163704c794dbe710644270010b02475662b3089c75aced" | ||
| dependencies = [ | ||
@@ -519,3 +518,3 @@ "lexe-hex", | ||
| name = "lexe-common" | ||
| version = "0.1.10" | ||
| version = "0.1.11" | ||
| dependencies = [ | ||
@@ -560,3 +559,3 @@ "anyhow", | ||
| "tempfile", | ||
| "thiserror 2.0.17", | ||
| "thiserror", | ||
| "tracing", | ||
@@ -569,5 +568,5 @@ "x509-parser", | ||
| name = "lexe-crypto" | ||
| version = "0.1.10" | ||
| version = "0.1.11" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "617f6607cddc45c634448a8bdd5be5b04c717c0e82fc4e61f0b8f24cace419e0" | ||
| checksum = "24c62725f40b08c48dd4537146f58da6a2bcba85a37f9f58a4c1fde76aafdefc" | ||
| dependencies = [ | ||
@@ -590,5 +589,5 @@ "bcs", | ||
| name = "lexe-enclave" | ||
| version = "0.1.10" | ||
| version = "0.1.11" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "475c422b55acec72c6dc8e4f590f4592ffb41175a6eaffa9601672a610e5c94b" | ||
| checksum = "061b46d8f6185130d8f2ee99391bf3005d2ff5ba89cfc1eb6e8ea55076142c28" | ||
| dependencies = [ | ||
@@ -610,11 +609,11 @@ "cfg-if", | ||
| name = "lexe-hex" | ||
| version = "0.1.10" | ||
| version = "0.1.11" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "24701aeca927296de900880d136ed506cbe2954d6c7753c9304716edb36c2212" | ||
| checksum = "512353b1642194139d0a3238530ca67641e8461c0231c6cb28ceb460648fed51" | ||
| [[package]] | ||
| name = "lexe-serde" | ||
| version = "0.1.10" | ||
| version = "0.1.11" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "50815113d9628a3f2b4b0a5ea0e9d75725a8888ac7fcef8f095985b995b997d5" | ||
| checksum = "1c3c0b52888c4e98b7d66abccfd9797fd2f7761a82d7b4acae88202ee7fcaa0b" | ||
| dependencies = [ | ||
@@ -628,5 +627,5 @@ "base64", | ||
| name = "lexe-sha256" | ||
| version = "0.1.10" | ||
| version = "0.1.11" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "13b6b5a23789e03ac30956e9759e829377f0b8c2df57889224b7cedee74429b6" | ||
| checksum = "09a24e8711cf9f4cff70f780e58cf59690168d7457c79343923892bf2c527ed4" | ||
| dependencies = [ | ||
@@ -641,5 +640,5 @@ "lexe-byte-array", | ||
| name = "lexe-std" | ||
| version = "0.1.10" | ||
| version = "0.1.11" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "00b6f5d9df00298a4cb5092473bdc2e0a33b586bc50bf0c8bb82a866b54c6fbb" | ||
| checksum = "027437edc551760845f3d89a55782eee333ff2125a2d3d44d77d26df07cf5f10" | ||
| dependencies = [ | ||
@@ -1200,11 +1199,2 @@ "ref-cast", | ||
| name = "thiserror" | ||
| version = "1.0.69" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" | ||
| dependencies = [ | ||
| "thiserror-impl 1.0.69", | ||
| ] | ||
| [[package]] | ||
| name = "thiserror" | ||
| version = "2.0.17" | ||
@@ -1214,3 +1204,3 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| dependencies = [ | ||
| "thiserror-impl 2.0.17", | ||
| "thiserror-impl", | ||
| ] | ||
@@ -1220,13 +1210,2 @@ | ||
| name = "thiserror-impl" | ||
| version = "1.0.69" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" | ||
| dependencies = [ | ||
| "proc-macro2", | ||
| "quote", | ||
| "syn", | ||
| ] | ||
| [[package]] | ||
| name = "thiserror-impl" | ||
| version = "2.0.17" | ||
@@ -1541,3 +1520,3 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| "rusticata-macros", | ||
| "thiserror 2.0.17", | ||
| "thiserror", | ||
| "time", | ||
@@ -1544,0 +1523,0 @@ ] |
+11
-11
@@ -16,3 +16,3 @@ # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO | ||
| name = "lexe-common" | ||
| version = "0.1.10" | ||
| version = "0.1.11" | ||
| authors = [ | ||
@@ -65,3 +65,3 @@ "Max Fang <max@lexe.app>", | ||
| [dependencies.bcs] | ||
| version = "0.1" | ||
| version = "0.2" | ||
@@ -107,21 +107,21 @@ [dependencies.bip39] | ||
| [dependencies.lexe-byte-array] | ||
| version = "0.1.10" | ||
| version = "0.1.11" | ||
| [dependencies.lexe-crypto] | ||
| version = "0.1.10" | ||
| version = "0.1.11" | ||
| [dependencies.lexe-enclave] | ||
| version = "0.1.10" | ||
| version = "0.1.11" | ||
| [dependencies.lexe-hex] | ||
| version = "0.1.10" | ||
| version = "0.1.11" | ||
| [dependencies.lexe-serde] | ||
| version = "0.1.10" | ||
| version = "0.1.11" | ||
| [dependencies.lexe-sha256] | ||
| version = "0.1.10" | ||
| version = "0.1.11" | ||
| [dependencies.lexe-std] | ||
| version = "0.1.10" | ||
| version = "0.1.11" | ||
| features = ["ref-cast"] | ||
@@ -216,7 +216,7 @@ | ||
| [dev-dependencies.lexe-crypto] | ||
| version = "0.1.10" | ||
| version = "0.1.11" | ||
| features = ["test-utils"] | ||
| [dev-dependencies.lexe-enclave] | ||
| version = "0.1.10" | ||
| version = "0.1.11" | ||
| features = ["test-utils"] | ||
@@ -223,0 +223,0 @@ |
+3
-1
@@ -6,2 +6,4 @@ use std::{include_bytes, time::Duration}; | ||
| use crate::{ppm, ppm::Ppm}; | ||
| // --- General --- // | ||
@@ -91,3 +93,3 @@ | ||
| /// [`lightning::util::config::ChannelHandshakeConfig::their_channel_reserve_proportional_millionths`] | ||
| pub const LSP_RESERVE_PROP_PPM: u32 = 10_000; // 1% | ||
| pub const LSP_RESERVE_PROPORTION: Ppm = ppm!(1.0%); | ||
@@ -94,0 +96,0 @@ /// The LSP will only accept new inbound channels with channel value at or above |
+186
-23
@@ -22,19 +22,24 @@ //! A "parts per million" (ppm) newtype for proportional fee rates. | ||
| //! | ||
| //! Use [`Ppm::new`](crate::ppm::Ppm::new) for compile-time validated constants: | ||
| //! Use the [`ppm!`] macro for convenient compile-time validated constants: | ||
| //! | ||
| //! ``` | ||
| //! # use lexe_common::ppm::Ppm; | ||
| //! const MY_FEE_RATE: Ppm = Ppm::new(3000); // 0.3% | ||
| //! # use lexe_common::{ppm, ppm::Ppm}; | ||
| //! # use rust_decimal::Decimal; | ||
| //! const A_FEE_RATE_PPM: Ppm = ppm!(3000); // 0.3% | ||
| //! const B_FEE_RATE_PPM: Ppm = ppm!(0.3%); // 0.3% | ||
| //! const C_FEE_RATE_DEC: Decimal = ppm!(3000).to_decimal(); // 0.3% | ||
| //! ``` | ||
| //! | ||
| //! ### Converting to a decimal rate | ||
| //! ### Converting to a decimal rate or percentage | ||
| //! | ||
| //! [`Ppm::to_decimal`](crate::ppm::Ppm::to_decimal) returns a | ||
| //! [`Decimal`](rust_decimal::Decimal) rate: | ||
| //! [`Decimal`](rust_decimal::Decimal) rate, while | ||
| //! [`Ppm::to_percent`](crate::ppm::Ppm::to_percent) returns a percentage: | ||
| //! | ||
| //! ``` | ||
| //! # use lexe_common::ppm::Ppm; | ||
| //! # use lexe_common::{ppm, ppm::Ppm}; | ||
| //! # use lexe_common::dec; | ||
| //! let rate = Ppm::new(5000).to_decimal(); // 0.5% | ||
| //! assert_eq!(rate, dec!(0.005)); | ||
| //! let ppm = ppm!(5000); | ||
| //! assert_eq!(ppm.to_decimal(), dec!(0.005)); | ||
| //! assert_eq!(ppm.to_percent(), dec!(0.5)); | ||
| //! ``` | ||
@@ -50,2 +55,20 @@ | ||
| /// A convenient, const-friendly way to build a [`Ppm`] from a ppm literal | ||
| /// or percent literal. | ||
| /// | ||
| /// Ex: `ppm!(1230)` -> `Ppm::new(1230)` | ||
| /// Ex: `ppm!(0.123%)` -> `Ppm::new(1230)` | ||
| #[macro_export] | ||
| macro_rules! ppm { | ||
| ($whole:tt . $frac:tt %) => { | ||
| const { $crate::ppm::Ppm::const_from_percent($crate::dec!($whole . $frac)) } | ||
| }; | ||
| ($whole:tt %) => { | ||
| const { $crate::ppm::Ppm::const_from_percent($crate::dec!($whole)) } | ||
| }; | ||
| ($amount:expr) => { | ||
| const { $crate::ppm::Ppm::new($amount) } | ||
| } | ||
| } | ||
| /// Errors that can occur when constructing a [`Ppm`]. | ||
@@ -89,2 +112,80 @@ #[derive(Debug, thiserror::Error)] | ||
| /// Construct a [`Ppm`] from a [`Decimal`] percentage value. | ||
| /// | ||
| /// # Panics | ||
| /// | ||
| /// Panics at compile time (in const context) or runtime if `pct` is | ||
| /// outside the valid range `[0.0000, 100.0000]`, or if it is not exactly | ||
| /// representable as a whole PPM value (e.g., `0.00001%`). | ||
| #[doc(hidden)] | ||
| pub const fn const_from_percent(pct: Decimal) -> Self { | ||
| // `scale + 2` == "move the decimal left 2 places" == `pct / 100.0` | ||
| let lo = pct.mantissa() as u32; | ||
| let mid = 0; | ||
| let hi = 0; | ||
| Self::const_from_decimal(Decimal::from_parts( | ||
| lo, | ||
| mid, | ||
| hi, | ||
| false, | ||
| pct.scale() + 2, | ||
| )) | ||
| } | ||
| /// Construct a [`Ppm`] from a [`Decimal`] value. | ||
| /// | ||
| /// # Panics | ||
| /// | ||
| /// Panics at compile time (in const context) or runtime if `dec` is | ||
| /// outside the valid range `[0.000000, 1.000000]` and is not exactly | ||
| /// representable as a PPM (e.g., `0.0000001` requires too much precision). | ||
| #[doc(hidden)] | ||
| const fn const_from_decimal(dec: Decimal) -> Self { | ||
| let scale = dec.scale(); | ||
| // If scale <= 6, then `dec` has <= 6 digits after the decimal point | ||
| if scale <= 6 { | ||
| let exp = 6 - scale; | ||
| let base = 10_i128.pow(exp); | ||
| let ppm = dec.mantissa() * base; | ||
| Ppm::new(ppm as i32) | ||
| } else { | ||
| // `dec` has >6 digits after the decimal point (though the extra | ||
| // digits may be `0`) | ||
| let exp = scale - 6; | ||
| let base = 10_i128.pow(exp); | ||
| let mantissa = dec.mantissa(); | ||
| // If there is a remainder, then `dec` has extra non-zero digits | ||
| // and so requires too much precision for a whole PPM repr | ||
| assert!(mantissa % base == 0); | ||
| let ppm = mantissa / base; | ||
| Ppm::new(ppm as i32) | ||
| } | ||
| } | ||
| /// Construct a [`Ppm`] from a [`Decimal`] value. | ||
| /// | ||
| /// The decimal is multiplied by 1_000_000 and rounded to the nearest | ||
| /// integer. For example, `0.005` (0.5%) becomes 5000 ppm. | ||
| /// | ||
| /// Returns an error if the input is negative or exceeds `1.0`. | ||
| pub fn try_from_decimal(rate: Decimal) -> Result<Self, Error> { | ||
| use rust_decimal::prelude::ToPrimitive; | ||
| let ppm_dec = (rate * dec!(1_000_000)).round(); | ||
| let ppm_i32 = ppm_dec.to_i32().ok_or(Error::TooLarge)?; | ||
| Self::try_from_inner(ppm_i32) | ||
| } | ||
| /// Construct a [`Ppm`] from a [`Decimal`] percentage value. | ||
| /// | ||
| /// The decimal is multiplied by 10_000 and rounded to the nearest | ||
| /// integer. For example, `0.5` (0.5%) becomes 5000 ppm. | ||
| /// | ||
| /// Returns an error if the input is negative or exceeds `100.0` (100%). | ||
| pub fn try_from_percent(pct: Decimal) -> Result<Self, Error> { | ||
| Self::try_from_decimal(pct / dec!(100)) | ||
| } | ||
| /// Returns the ppm value as an `i32`. | ||
@@ -106,6 +207,26 @@ #[inline] | ||
| #[inline] | ||
| pub fn to_decimal(self) -> Decimal { | ||
| Decimal::from(self.0) / dec!(1_000_000) | ||
| pub const fn to_decimal(self) -> Decimal { | ||
| // This is `Decimal::from(self.0) / dec!(1_000_000)` but works in a | ||
| // `const` context. | ||
| let lo = self.to_u32(); | ||
| let mid = 0; | ||
| let hi = 0; | ||
| let negative = false; | ||
| let scale = 6; | ||
| Decimal::from_parts(lo, mid, hi, negative, scale) | ||
| } | ||
| /// Returns the ppm value as a [`Decimal`] percentage (ppm / 10_000). | ||
| /// | ||
| /// For example, 5000 ppm becomes `0.5` (0.5%). | ||
| #[inline] | ||
| pub const fn to_percent(self) -> Decimal { | ||
| let lo = self.to_u32(); | ||
| let mid = 0; | ||
| let hi = 0; | ||
| let negative = false; | ||
| let scale = 4; | ||
| Decimal::from_parts(lo, mid, hi, negative, scale) | ||
| } | ||
| /// Checks bounds, returning [`Self`] if the value is valid. | ||
@@ -182,2 +303,3 @@ #[inline] | ||
| #[inline] | ||
| fn try_from(value: i32) -> Result<Self, Self::Error> { | ||
@@ -200,14 +322,5 @@ Self::try_from_inner(value) | ||
| /// Construct a [`Ppm`] from a [`Decimal`] rate. | ||
| /// | ||
| /// The decimal is multiplied by 1_000_000 and rounded to the nearest | ||
| /// integer. For example, `0.005` (0.5%) becomes 5000 ppm. | ||
| /// | ||
| /// Returns an error if the result is negative or exceeds 1_000_000. | ||
| #[inline] | ||
| fn try_from(rate: Decimal) -> Result<Self, Self::Error> { | ||
| use rust_decimal::prelude::ToPrimitive; | ||
| let ppm_dec = (rate * dec!(1_000_000)).round(); | ||
| let ppm_i32 = ppm_dec.to_i32().ok_or(Error::TooLarge)?; | ||
| Self::try_from_inner(ppm_i32) | ||
| Self::try_from_decimal(rate) | ||
| } | ||
@@ -262,2 +375,3 @@ } | ||
| use super::*; | ||
| use crate::ppm; | ||
@@ -275,2 +389,22 @@ #[test] | ||
| #[test] | ||
| fn macros() { | ||
| assert_eq!(ppm!(0), Ppm::ZERO); | ||
| assert_eq!(ppm!(1230), Ppm::new(1230)); | ||
| assert_eq!(ppm!(1_000_000), Ppm::new(1_000_000)); | ||
| assert_eq!(ppm!(0%), Ppm::ZERO); | ||
| assert_eq!(ppm!(0.0%), Ppm::ZERO); | ||
| assert_eq!(ppm!(0.123%), Ppm::new(1230)); | ||
| assert_eq!(ppm!(0.1230%), Ppm::new(1230)); | ||
| assert_eq!(ppm!(0.12300%), Ppm::new(1230)); | ||
| assert_eq!(ppm!(0.3%), Ppm::new(3000)); | ||
| assert_eq!(ppm!(1%), Ppm::new(10_000)); | ||
| assert_eq!(ppm!(1.0%), Ppm::new(10_000)); | ||
| assert_eq!(ppm!(50%), Ppm::new(500_000)); | ||
| assert_eq!(ppm!(100%), Ppm::MAX); | ||
| assert_eq!(ppm!(100.0%), Ppm::MAX); | ||
| assert_eq!(ppm!(0.0001%), Ppm::new(1)); | ||
| } | ||
| #[test] | ||
| fn to_decimal() { | ||
@@ -286,2 +420,13 @@ assert_eq!(Ppm::ZERO.to_decimal(), dec!(0)); | ||
| #[test] | ||
| fn to_percent() { | ||
| assert_eq!(Ppm::ZERO.to_percent(), dec!(0)); | ||
| assert_eq!(Ppm::new(1).to_percent(), dec!(0.0001)); | ||
| assert_eq!(Ppm::new(1000).to_percent(), dec!(0.1)); | ||
| assert_eq!(Ppm::new(3000).to_percent(), dec!(0.3)); | ||
| assert_eq!(Ppm::new(10_000).to_percent(), dec!(1)); | ||
| assert_eq!(Ppm::new(100_000).to_percent(), dec!(10)); | ||
| assert_eq!(Ppm::MAX.to_percent(), dec!(100)); | ||
| } | ||
| #[test] | ||
| fn try_from_decimal() { | ||
@@ -396,6 +541,24 @@ // Basic conversions | ||
| // Roundtrip: Ppm -> Decimal -> Ppm | ||
| let roundtrip = Ppm::try_from(dec).unwrap(); | ||
| prop_assert_eq!(ppm, roundtrip); | ||
| prop_assert_eq!(ppm, Ppm::try_from(dec).unwrap()); | ||
| prop_assert_eq!(ppm, Ppm::try_from_decimal(dec).unwrap()); | ||
| prop_assert_eq!(ppm, Ppm::const_from_decimal(dec)); | ||
| }); | ||
| } | ||
| #[test] | ||
| fn proptest_percent_roundtrip() { | ||
| proptest!(|(ppm in any::<Ppm>())| { | ||
| let pct = ppm.to_percent(); | ||
| // Percent is in [0, 100] | ||
| prop_assert!(pct >= Decimal::ZERO); | ||
| prop_assert!(pct <= dec!(100)); | ||
| // Roundtrip: Ppm -> percent -> Ppm | ||
| prop_assert_eq!(ppm, Ppm::try_from_percent(pct).unwrap()); | ||
| prop_assert_eq!(ppm, Ppm::const_from_percent(pct)); | ||
| prop_assert_eq!(pct, ppm.to_decimal() * dec!(100.0)); | ||
| }); | ||
| } | ||
| } |
Sorry, the diff of this file is not supported yet