From 6e4cf78d615ff98ea1931cda57449ec461c11ec2 Mon Sep 17 00:00:00 2001 From: 0xgleb Date: Wed, 27 Aug 2025 15:11:29 -0300 Subject: [PATCH 1/6] expose in rust --- crates/float/src/lib.rs | 169 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) diff --git a/crates/float/src/lib.rs b/crates/float/src/lib.rs index b88b69f6..0bffd8ee 100644 --- a/crates/float/src/lib.rs +++ b/crates/float/src/lib.rs @@ -335,6 +335,136 @@ impl Float { Ok(Float(bytes)) } + /// Returns the maximum positive value that can be represented as a `Float`. + /// + /// # Returns + /// + /// * `Ok(Float)` - The maximum positive value. + /// * `Err(FloatError)` - If the EVM call fails. + /// + /// # Example + /// + /// ``` + /// use rain_math_float::Float; + /// + /// let max_pos = Float::max_positive_value()?; + /// let zero = Float::parse("0".to_string())?; + /// + /// // Max positive is greater than zero + /// assert!(max_pos.gt(zero)?); + /// + /// // Max positive is greater than any normal large number + /// let big_number = Float::parse("999999999999999999999".to_string())?; + /// assert!(max_pos.gt(big_number)?); + /// + /// anyhow::Ok(()) + /// ``` + pub fn max_positive_value() -> Result { + let calldata = DecimalFloat::maxPositiveValueCall {}.abi_encode(); + + execute_call(Bytes::from(calldata), |output| { + let decoded = DecimalFloat::maxPositiveValueCall::abi_decode_returns(output.as_ref())?; + Ok(Float(decoded)) + }) + } + + /// Returns the minimum positive value that can be represented as a `Float`. + /// + /// # Returns + /// + /// * `Ok(Float)` - The minimum positive value. + /// * `Err(FloatError)` - If the EVM call fails. + /// + /// # Example + /// + /// ``` + /// use rain_math_float::Float; + /// + /// let min_pos = Float::min_positive_value()?; + /// let zero = Float::parse("0".to_string())?; + /// + /// // Min positive is greater than zero but smaller than any other positive number + /// assert!(min_pos.gt(zero)?); + /// + /// let small_number = Float::parse("0.000000000000000001".to_string())?; + /// assert!(min_pos.lt(small_number)?); + /// + /// anyhow::Ok(()) + /// ``` + pub fn min_positive_value() -> Result { + let calldata = DecimalFloat::minPositiveValueCall {}.abi_encode(); + + execute_call(Bytes::from(calldata), |output| { + let decoded = DecimalFloat::minPositiveValueCall::abi_decode_returns(output.as_ref())?; + Ok(Float(decoded)) + }) + } + + /// Returns the maximum negative value that can be represented as a `Float`. + /// + /// # Returns + /// + /// * `Ok(Float)` - The maximum negative value (closest to zero). + /// * `Err(FloatError)` - If the EVM call fails. + /// + /// # Example + /// + /// ``` + /// use rain_math_float::Float; + /// + /// let max_neg = Float::max_negative_value()?; + /// let zero = Float::parse("0".to_string())?; + /// + /// // Max negative is less than zero but greater than any other negative number + /// assert!(max_neg.lt(zero)?); + /// + /// let small_negative = Float::parse("-0.000000000000000001".to_string())?; + /// assert!(max_neg.gt(small_negative)?); + /// + /// anyhow::Ok(()) + /// ``` + pub fn max_negative_value() -> Result { + let calldata = DecimalFloat::maxNegativeValueCall {}.abi_encode(); + + execute_call(Bytes::from(calldata), |output| { + let decoded = DecimalFloat::maxNegativeValueCall::abi_decode_returns(output.as_ref())?; + Ok(Float(decoded)) + }) + } + + /// Returns the minimum negative value that can be represented as a `Float`. + /// + /// # Returns + /// + /// * `Ok(Float)` - The minimum negative value (furthest from zero). + /// * `Err(FloatError)` - If the EVM call fails. + /// + /// # Example + /// + /// ``` + /// use rain_math_float::Float; + /// + /// let min_neg = Float::min_negative_value()?; + /// let zero = Float::parse("0".to_string())?; + /// + /// // Min negative is less than zero + /// assert!(min_neg.lt(zero)?); + /// + /// // Min negative is less than any normal negative number + /// let big_negative = Float::parse("-999999999999999999999".to_string())?; + /// assert!(min_neg.lt(big_negative)?); + /// + /// anyhow::Ok(()) + /// ``` + pub fn min_negative_value() -> Result { + let calldata = DecimalFloat::minNegativeValueCall {}.abi_encode(); + + execute_call(Bytes::from(calldata), |output| { + let decoded = DecimalFloat::minNegativeValueCall::abi_decode_returns(output.as_ref())?; + Ok(Float(decoded)) + }) + } + /// Formats the float as a decimal string. /// /// NOTE: Uses 18 decimal places and fails if the float has more than @@ -1061,6 +1191,45 @@ mod tests { )); } + #[test] + fn test_float_constants() { + // Test that all constant methods return valid floats + let max_pos = Float::max_positive_value().unwrap(); + let min_pos = Float::min_positive_value().unwrap(); + let max_neg = Float::max_negative_value().unwrap(); + let min_neg = Float::min_negative_value().unwrap(); + + // Verify they return expected hex values (these are extreme values that may not format well) + assert_eq!( + max_pos.as_hex(), + "0x7fffffff7fffffffffffffffffffffffffffffffffffffffffffffffffffffff" + ); + assert_eq!( + min_pos.as_hex(), + "0x8000000000000000000000000000000000000000000000000000000000000001" + ); + assert_eq!( + max_neg.as_hex(), + "0x80000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + ); + assert_eq!( + min_neg.as_hex(), + "0x7fffffff80000000000000000000000000000000000000000000000000000000" + ); + + // Verify logical relationships between constants + let zero = Float::parse("0".to_string()).unwrap(); + + // max_pos > min_pos (but these extreme values might not compare properly) + // Instead just verify they are different values + assert_ne!(max_pos.as_hex(), min_pos.as_hex()); + assert_ne!(max_neg.as_hex(), min_neg.as_hex()); + + // Test that we can use these constants in basic operations + assert!(min_pos.gt(zero).unwrap()); // min positive should be > 0 + assert!(max_neg.lt(zero).unwrap()); // max negative should be < 0 + } + proptest! { #[test] fn test_format_parse(float in reasonable_float()) { From 5d250124ff68c7b37a7013fa2e71fbf751d0f99d Mon Sep 17 00:00:00 2001 From: 0xgleb Date: Wed, 27 Aug 2025 15:12:41 -0300 Subject: [PATCH 2/6] add property testing --- crates/float/src/lib.rs | 69 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/crates/float/src/lib.rs b/crates/float/src/lib.rs index 0bffd8ee..dd8ecdae 100644 --- a/crates/float/src/lib.rs +++ b/crates/float/src/lib.rs @@ -1,7 +1,7 @@ use alloy::hex::FromHex; -use alloy::primitives::{B256, Bytes}; +use alloy::primitives::{Bytes, B256}; use alloy::{sol, sol_types::SolCall}; -use revm::primitives::{U256, fixed_bytes}; +use revm::primitives::{fixed_bytes, U256}; use serde::{Deserialize, Serialize}; use std::ops::{Add, Div, Mul, Neg, Sub}; use wasm_bindgen_utils::prelude::*; @@ -1907,4 +1907,69 @@ mod tests { assert_eq!(fixed, value / U256::from(10)); } } + + proptest! { + #[test] + fn test_constants_relationships(float in reasonable_float()) { + let max_pos = Float::max_positive_value().unwrap(); + let min_pos = Float::min_positive_value().unwrap(); + let max_neg = Float::max_negative_value().unwrap(); + let min_neg = Float::min_negative_value().unwrap(); + let zero = Float::parse("0".to_string()).unwrap(); + + // Test that constants are the extremes + // Any reasonable positive float should be <= max_positive and >= min_positive + if float.gt(zero).unwrap() { + prop_assert!(float.lte(max_pos).unwrap()); + prop_assert!(float.gte(min_pos).unwrap()); + } + + // Any reasonable negative float should be <= max_negative and >= min_negative + // (max_negative is closest to zero, min_negative is furthest from zero) + if float.lt(zero).unwrap() { + prop_assert!(float.lte(max_neg).unwrap()); + prop_assert!(float.gte(min_neg).unwrap()); + } + + // Constants should be consistent regardless of arbitrary float + prop_assert!(max_pos.gt(zero).unwrap()); + prop_assert!(min_pos.gt(zero).unwrap()); + prop_assert!(max_neg.lt(zero).unwrap()); + prop_assert!(min_neg.lt(zero).unwrap()); + + // Verify constants maintain their ordering + prop_assert!(min_pos.lt(max_pos).unwrap()); + prop_assert!(min_neg.lt(max_neg).unwrap()); + prop_assert!(max_neg.lt(zero).unwrap()); + prop_assert!(min_pos.gt(zero).unwrap()); + } + } + + proptest! { + #[test] + fn test_constants_edge_cases(float in arb_float()) { + let max_pos = Float::max_positive_value().unwrap(); + let min_pos = Float::min_positive_value().unwrap(); + let max_neg = Float::max_negative_value().unwrap(); + let min_neg = Float::min_negative_value().unwrap(); + + // Constants should always be distinct + prop_assert!(!max_pos.eq(min_pos).unwrap()); + prop_assert!(!max_neg.eq(min_neg).unwrap()); + prop_assert!(!max_pos.eq(max_neg).unwrap()); + prop_assert!(!min_pos.eq(min_neg).unwrap()); + + // Test that constants are at the boundaries + // (Note: We can't test arithmetic operations that would overflow/underflow + // since those would fail, but we can test comparisons) + + // No arbitrary float should be greater than max_pos or less than min_neg + if !float.eq(max_pos).unwrap() { + prop_assert!(!float.gt(max_pos).unwrap()); + } + if !float.eq(min_neg).unwrap() { + prop_assert!(!float.lt(min_neg).unwrap()); + } + } + } } From 84261586292f94e8f076edcc1b8f9f3cf9440acc Mon Sep 17 00:00:00 2001 From: 0xgleb Date: Wed, 27 Aug 2025 15:13:01 -0300 Subject: [PATCH 3/6] expose to ts --- crates/float/src/js_api.rs | 88 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/crates/float/src/js_api.rs b/crates/float/src/js_api.rs index bc88a3fe..f586cd73 100644 --- a/crates/float/src/js_api.rs +++ b/crates/float/src/js_api.rs @@ -290,6 +290,94 @@ impl Float { Self::from_hex(hex) } + /// Returns the maximum positive value that can be represented as a `Float`. + /// + /// # Returns + /// + /// * `Ok(Float)` - The maximum positive value. + /// * `Err(FloatError)` - If the EVM call fails. + /// + /// # Example + /// + /// ```typescript + /// const maxPosResult = Float.maxPositiveValue(); + /// if (maxPosResult.error) { + /// console.error(maxPosResult.error); + /// } + /// const maxPos = maxPosResult.value; + /// assert(!maxPos.format().error); + /// ``` + #[wasm_export(js_name = "maxPositiveValue", preserve_js_class)] + pub fn max_positive_value_js() -> Result { + Self::max_positive_value() + } + + /// Returns the minimum positive value that can be represented as a `Float`. + /// + /// # Returns + /// + /// * `Ok(Float)` - The minimum positive value. + /// * `Err(FloatError)` - If the EVM call fails. + /// + /// # Example + /// + /// ```typescript + /// const minPosResult = Float.minPositiveValue(); + /// if (minPosResult.error) { + /// console.error(minPosResult.error); + /// } + /// const minPos = minPosResult.value; + /// assert(!minPos.format().error); + /// ``` + #[wasm_export(js_name = "minPositiveValue", preserve_js_class)] + pub fn min_positive_value_js() -> Result { + Self::min_positive_value() + } + + /// Returns the maximum negative value that can be represented as a `Float`. + /// + /// # Returns + /// + /// * `Ok(Float)` - The maximum negative value (closest to zero). + /// * `Err(FloatError)` - If the EVM call fails. + /// + /// # Example + /// + /// ```typescript + /// const maxNegResult = Float.maxNegativeValue(); + /// if (maxNegResult.error) { + /// console.error(maxNegResult.error); + /// } + /// const maxNeg = maxNegResult.value; + /// assert(!maxNeg.format().error); + /// ``` + #[wasm_export(js_name = "maxNegativeValue", preserve_js_class)] + pub fn max_negative_value_js() -> Result { + Self::max_negative_value() + } + + /// Returns the minimum negative value that can be represented as a `Float`. + /// + /// # Returns + /// + /// * `Ok(Float)` - The minimum negative value (furthest from zero). + /// * `Err(FloatError)` - If the EVM call fails. + /// + /// # Example + /// + /// ```typescript + /// const minNegResult = Float.minNegativeValue(); + /// if (minNegResult.error) { + /// console.error(minNegResult.error); + /// } + /// const minNeg = minNegResult.value; + /// assert(!minNeg.format().error); + /// ``` + #[wasm_export(js_name = "minNegativeValue", preserve_js_class)] + pub fn min_negative_value_js() -> Result { + Self::min_negative_value() + } + /// Formats the float as a decimal string. /// /// NOTE: Uses 18 decimal places and fails if the float has more than From 15ac358506ef77734b577e78ebb2832feedd186e Mon Sep 17 00:00:00 2001 From: 0xgleb Date: Wed, 27 Aug 2025 15:13:16 -0300 Subject: [PATCH 4/6] test ts bindings --- test_js/float.test.ts | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/test_js/float.test.ts b/test_js/float.test.ts index 54a09996..e9d42e30 100644 --- a/test_js/float.test.ts +++ b/test_js/float.test.ts @@ -109,5 +109,43 @@ describe('Test Float Bindings', () => { expect(a.div(b)?.value!.format()?.value!).toBe('-1'); expect(a.add(b)?.value!.format()?.value!).toBe('0'); }); + + it('should test float constants', () => { + // Test that all constant methods return valid floats + const maxPosResult = Float.maxPositiveValue(); + const minPosResult = Float.minPositiveValue(); + const maxNegResult = Float.maxNegativeValue(); + const minNegResult = Float.minNegativeValue(); + + // Verify no errors occurred + expect(maxPosResult.error).toBeUndefined(); + expect(minPosResult.error).toBeUndefined(); + expect(maxNegResult.error).toBeUndefined(); + expect(minNegResult.error).toBeUndefined(); + + const maxPos = maxPosResult.value!; + const minPos = minPosResult.value!; + const maxNeg = maxNegResult.value!; + const minNeg = minNegResult.value!; + + // Verify they return expected hex values (these are extreme values) + expect(maxPos.asHex()).toBe('0x7fffffff7fffffffffffffffffffffffffffffffffffffffffffffffffffffff'); + expect(minPos.asHex()).toBe('0x8000000000000000000000000000000000000000000000000000000000000001'); + expect(maxNeg.asHex()).toBe('0x80000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff'); + expect(minNeg.asHex()).toBe('0x7fffffff80000000000000000000000000000000000000000000000000000000'); + + // Verify they are different values + expect(maxPos.asHex()).not.toBe(minPos.asHex()); + expect(maxNeg.asHex()).not.toBe(minNeg.asHex()); + + // Test basic relationships with zero + const zero = Float.fromBigint(0n); + + // min_pos > 0 + expect(minPos.gt(zero)?.value!).toBe(true); + + // max_neg < 0 (max negative is closest to zero) + expect(maxNeg.lt(zero)?.value!).toBe(true); + }); } }); From da5078570a06535f7464b6f663ada17a9434a8f3 Mon Sep 17 00:00:00 2001 From: 0xgleb Date: Wed, 27 Aug 2025 15:22:54 -0300 Subject: [PATCH 5/6] fix rs static --- crates/float/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/float/src/lib.rs b/crates/float/src/lib.rs index dd8ecdae..d21c05c7 100644 --- a/crates/float/src/lib.rs +++ b/crates/float/src/lib.rs @@ -1,7 +1,7 @@ use alloy::hex::FromHex; -use alloy::primitives::{Bytes, B256}; +use alloy::primitives::{B256, Bytes}; use alloy::{sol, sol_types::SolCall}; -use revm::primitives::{fixed_bytes, U256}; +use revm::primitives::{U256, fixed_bytes}; use serde::{Deserialize, Serialize}; use std::ops::{Add, Div, Mul, Neg, Sub}; use wasm_bindgen_utils::prelude::*; From e7d667f7bd95286fcbdbe233d076dfea15e05b49 Mon Sep 17 00:00:00 2001 From: 0xgleb Date: Wed, 27 Aug 2025 15:28:10 -0300 Subject: [PATCH 6/6] avoid leaking binary rep --- crates/float/src/lib.rs | 50 +++++++++++++++++++++-------------------- test_js/float.test.ts | 43 +++++++++++++++++++++-------------- 2 files changed, 52 insertions(+), 41 deletions(-) diff --git a/crates/float/src/lib.rs b/crates/float/src/lib.rs index d21c05c7..724dd29b 100644 --- a/crates/float/src/lib.rs +++ b/crates/float/src/lib.rs @@ -1199,35 +1199,37 @@ mod tests { let max_neg = Float::max_negative_value().unwrap(); let min_neg = Float::min_negative_value().unwrap(); - // Verify they return expected hex values (these are extreme values that may not format well) - assert_eq!( - max_pos.as_hex(), - "0x7fffffff7fffffffffffffffffffffffffffffffffffffffffffffffffffffff" - ); - assert_eq!( - min_pos.as_hex(), - "0x8000000000000000000000000000000000000000000000000000000000000001" - ); - assert_eq!( - max_neg.as_hex(), - "0x80000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff" - ); - assert_eq!( - min_neg.as_hex(), - "0x7fffffff80000000000000000000000000000000000000000000000000000000" - ); - - // Verify logical relationships between constants let zero = Float::parse("0".to_string()).unwrap(); - // max_pos > min_pos (but these extreme values might not compare properly) - // Instead just verify they are different values - assert_ne!(max_pos.as_hex(), min_pos.as_hex()); - assert_ne!(max_neg.as_hex(), min_neg.as_hex()); + // Test mathematical properties without exposing binary representation + + // All constants should be distinct + assert!(!max_pos.eq(min_pos).unwrap()); + assert!(!max_neg.eq(min_neg).unwrap()); + assert!(!max_pos.eq(max_neg).unwrap()); + assert!(!min_pos.eq(min_neg).unwrap()); - // Test that we can use these constants in basic operations + // Test sign properties assert!(min_pos.gt(zero).unwrap()); // min positive should be > 0 + assert!(max_pos.gt(zero).unwrap()); // max positive should be > 0 assert!(max_neg.lt(zero).unwrap()); // max negative should be < 0 + assert!(min_neg.lt(zero).unwrap()); // min negative should be < 0 + + // Test ordering relationships + assert!(min_pos.lt(max_pos).unwrap()); // min positive < max positive + assert!(min_neg.lt(max_neg).unwrap()); // min negative < max negative + + // Test boundary properties + let one = Float::parse("1".to_string()).unwrap(); + let neg_one = Float::parse("-1".to_string()).unwrap(); + + // Positive constants should be greater than normal values + assert!(max_pos.gt(one).unwrap()); + assert!(min_pos.lt(one).unwrap()); + + // Negative constants should be more extreme than normal negative values + assert!(max_neg.gt(neg_one).unwrap()); + assert!(min_neg.lt(neg_one).unwrap()); } proptest! { diff --git a/test_js/float.test.ts b/test_js/float.test.ts index e9d42e30..eaeeeb65 100644 --- a/test_js/float.test.ts +++ b/test_js/float.test.ts @@ -128,24 +128,33 @@ describe('Test Float Bindings', () => { const maxNeg = maxNegResult.value!; const minNeg = minNegResult.value!; - // Verify they return expected hex values (these are extreme values) - expect(maxPos.asHex()).toBe('0x7fffffff7fffffffffffffffffffffffffffffffffffffffffffffffffffffff'); - expect(minPos.asHex()).toBe('0x8000000000000000000000000000000000000000000000000000000000000001'); - expect(maxNeg.asHex()).toBe('0x80000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff'); - expect(minNeg.asHex()).toBe('0x7fffffff80000000000000000000000000000000000000000000000000000000'); - - // Verify they are different values - expect(maxPos.asHex()).not.toBe(minPos.asHex()); - expect(maxNeg.asHex()).not.toBe(minNeg.asHex()); - - // Test basic relationships with zero const zero = Float.fromBigint(0n); - - // min_pos > 0 - expect(minPos.gt(zero)?.value!).toBe(true); - - // max_neg < 0 (max negative is closest to zero) - expect(maxNeg.lt(zero)?.value!).toBe(true); + const one = Float.parse('1')?.value!; + const negOne = Float.parse('-1')?.value!; + + // Test mathematical properties without exposing binary representation + + // All constants should be distinct + expect(maxPos.eq(minPos)?.value!).toBe(false); + expect(maxNeg.eq(minNeg)?.value!).toBe(false); + expect(maxPos.eq(maxNeg)?.value!).toBe(false); + expect(minPos.eq(minNeg)?.value!).toBe(false); + + // Test sign properties + expect(minPos.gt(zero)?.value!).toBe(true); // min positive > 0 + expect(maxPos.gt(zero)?.value!).toBe(true); // max positive > 0 + expect(maxNeg.lt(zero)?.value!).toBe(true); // max negative < 0 + expect(minNeg.lt(zero)?.value!).toBe(true); // min negative < 0 + + // Test ordering relationships + expect(minPos.lt(maxPos)?.value!).toBe(true); // min positive < max positive + expect(minNeg.lt(maxNeg)?.value!).toBe(true); // min negative < max negative + + // Test boundary properties + expect(maxPos.gt(one)?.value!).toBe(true); // max positive > 1 + expect(minPos.lt(one)?.value!).toBe(true); // min positive < 1 + expect(maxNeg.gt(negOne)?.value!).toBe(true); // max negative > -1 + expect(minNeg.lt(negOne)?.value!).toBe(true); // min negative < -1 }); } });