Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 95 additions & 52 deletions crates/float/src/js_api.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,40 @@
use crate::{Float, FloatError};
use revm::primitives::{B256, U256};
use serde::{Deserialize, Serialize};
use std::{
ops::{Add, Div, Mul, Neg, Sub},
str::FromStr,
};
use wasm_bindgen_utils::prelude::{js_sys::BigInt, *};
use wasm_bindgen_utils::{
impl_wasm_traits,
prelude::{js_sys::BigInt, *},
};

#[wasm_bindgen]
pub struct FromFixedDecimalLossyResult {
float: Float,
lossless: bool,
}

#[wasm_bindgen]
impl FromFixedDecimalLossyResult {
#[wasm_bindgen(getter)]
pub fn float(&self) -> Float {
self.float
}

#[wasm_bindgen(getter)]
pub fn lossless(&self) -> bool {
self.lossless
}
}
Comment on lines +13 to +30
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider adding trait derives for consistency and ergonomics.

ToFixedDecimalLossyResult (lines 32-37) has derives for Serialize, Deserialize, Debug, Clone, PartialEq, and Tsify, plus impl_wasm_traits!. FromFixedDecimalLossyResult lacks these traits, which may limit debugging, testing, and serialization capabilities.

If the Float type supports these traits, apply this diff to add them:

+#[derive(Debug, Clone, PartialEq)]
 #[wasm_bindgen]
 pub struct FromFixedDecimalLossyResult {
     float: Float,
     lossless: bool,
 }

If serialization is needed for JS interop consistency, consider also adding Serialize, Deserialize, and Tsify (assuming Float supports them).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[wasm_bindgen]
pub struct FromFixedDecimalLossyResult {
float: Float,
lossless: bool,
}
#[wasm_bindgen]
impl FromFixedDecimalLossyResult {
#[wasm_bindgen(getter)]
pub fn float(&self) -> Float {
self.float
}
#[wasm_bindgen(getter)]
pub fn lossless(&self) -> bool {
self.lossless
}
}
#[derive(Debug, Clone, PartialEq)]
#[wasm_bindgen]
pub struct FromFixedDecimalLossyResult {
float: Float,
lossless: bool,
}
#[wasm_bindgen]
impl FromFixedDecimalLossyResult {
#[wasm_bindgen(getter)]
pub fn float(&self) -> Float {
self.float
}
#[wasm_bindgen(getter)]
pub fn lossless(&self) -> bool {
self.lossless
}
}
🤖 Prompt for AI Agents
In crates/float/src/js_api.rs around lines 13 to 30, FromFixedDecimalLossyResult
is missing the same trait derives and wasm helper impls as
ToFixedDecimalLossyResult; add derives (e.g., Serialize, Deserialize, Debug,
Clone, PartialEq, Tsify) and call impl_wasm_traits! for
FromFixedDecimalLossyResult (only if the contained Float type supports those
traits), ensuring the struct keeps #[wasm_bindgen] for JS export and that any
feature flags/imports needed for Serialize/Deserialize/Tsify are enabled.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not needed, only gonna be used from ts/js

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@0xgleb, understood! Since FromFixedDecimalLossyResult is only consumed by TypeScript/JavaScript and not used for Rust-side operations, the additional trait derives aren't necessary.


✏️ Learnings added
Learnt from: 0xgleb
PR: rainlanguage/rain.math.float#159
File: crates/float/src/js_api.rs:13-30
Timestamp: 2025-10-15T20:41:13.554Z
Learning: In the rain.math.float codebase, wasm_bindgen structs that are only returned to TypeScript/JavaScript (like `FromFixedDecimalLossyResult`) don't need Rust trait derives (Debug, Clone, PartialEq, Serialize, Deserialize, Tsify) beyond what's required for JS interop. Only add these derives when the struct is used for Rust-side operations like serialization, debugging, or testing.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: 0xgleb
PR: rainlanguage/rain.math.float#57
File: crates/float/src/lib.rs:316-328
Timestamp: 2025-06-18T09:10:41.740Z
Learning: In the rain.math.float codebase, the user prefers implementing standard Rust traits (like Neg) rather than creating redundant public methods when the trait already provides the needed functionality. Float implements Copy, so reference usage with operators is not a concern.


#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Tsify)]
pub struct ToFixedDecimalLossyResult {
pub value: String,
pub lossless: bool,
}
impl_wasm_traits!(ToFixedDecimalLossyResult);

#[wasm_bindgen]
impl Float {
Expand Down Expand Up @@ -57,6 +87,59 @@ impl Float {
pub fn from_bigint(value: BigInt) -> Float {
Self::try_from_bigint(value).unwrap_throw()
}

/// Converts a fixed-point decimal value to a `Float` using the specified number of decimals,
/// allowing lossy conversions and reporting whether precision was preserved.
///
/// This function attempts to convert a fixed-point decimal representation to a `Float`.
/// Unlike `fromFixedDecimal`, this method will not fail if precision is lost during conversion,
/// but instead reports the loss through the `lossless` flag in the result.
///
/// # Arguments
///
/// * `value` - The fixed-point decimal value as a bigint (e.g., 12345n for 123.45 with 2 decimals).
/// * `decimals` - The number of decimal places in the fixed-point representation (0-255).
///
/// # Returns
///
/// Returns a `FromFixedDecimalLossyResult` containing:
/// * `float` - The resulting `Float` value.
/// * `lossless` - Boolean flag indicating whether the conversion preserved all precision (true) or was lossy (false).
///
/// # Errors
///
/// Throws a `JsValue` error if:
/// * The bigint value cannot be converted to a string.
/// * The value string cannot be parsed as a valid U256.
/// * The underlying EVM conversion fails.
///
/// # Example
///
/// ```typescript
/// // Lossless conversion
/// const result = Float.fromFixedDecimalLossy(12345n, 2);
/// const float = result.float;
/// const wasLossless = result.lossless;
/// assert(float.format()?.value === "123.45");
/// assert(wasLossless === true);
///
/// // Potentially lossy conversion
/// const result2 = Float.fromFixedDecimalLossy(123456789012345678901234567890n, 18);
/// if (!result2.lossless) {
/// console.warn("Precision was lost during conversion");
/// }
/// ```
#[wasm_bindgen(js_name = "fromFixedDecimalLossy")]
pub fn from_fixed_decimal_lossy_js(
value: BigInt,
decimals: u8,
) -> Result<FromFixedDecimalLossyResult, JsValue> {
let value_str: String = value.to_string(10).map_err(|e| JsValue::from(&e))?.into();
let val = U256::from_str(&value_str).map_err(|e| JsValue::from_str(&e.to_string()))?;
Comment on lines +137 to +138
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Standardize error conversion patterns.

Lines 101 and 102 use different error conversion approaches:

  • Line 101: JsValue::from(&e)
  • Line 102: JsValue::from_str(&e.to_string())

For consistency with similar functions (e.g., from_fixed_decimal_js at line 189), use a uniform pattern:

-        let value_str: String = value.to_string(10).map_err(|e| JsValue::from(&e))?.into();
+        let value_str: String = value.to_string(10)?.into();
         let val = U256::from_str(&value_str).map_err(|e| JsValue::from_str(&e.to_string()))?;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let value_str: String = value.to_string(10).map_err(|e| JsValue::from(&e))?.into();
let val = U256::from_str(&value_str).map_err(|e| JsValue::from_str(&e.to_string()))?;
let value_str: String = value.to_string(10)?.into();
let val = U256::from_str(&value_str).map_err(|e| JsValue::from_str(&e.to_string()))?;
🤖 Prompt for AI Agents
In crates/float/src/js_api.rs around lines 101 to 102, the error conversions are
inconsistent: change the map_err on line 101 to use
JsValue::from_str(&e.to_string()) so both lines use the same pattern
(map_err(|e| JsValue::from_str(&e.to_string()))?), matching the style used in
from_fixed_decimal_js at line 189; ensure both error mappings return a
JsValue::from_str of the error's to_string().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not needed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@0xgleb, understood. I'll leave the error conversion as is.

let (float, lossless) = Float::from_fixed_decimal_lossy(val, decimals)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(FromFixedDecimalLossyResult { float, lossless })
}
}

#[wasm_export]
Expand Down Expand Up @@ -175,65 +258,25 @@ impl Float {
.map_err(|e| FloatError::JsSysError(e.to_string().into()))
}

/// Converts a fixed-point decimal value to a `Float` using the specified number of decimals lossy.
///
/// # Arguments
///
/// * `value` - The fixed-point decimal value as a `string`.
/// * `decimals` - The number of decimals in the fixed-point representation.
///
/// # Returns
///
/// * `Ok(Float)` - The resulting `Float` value.
/// * `Err(FloatError)` - If the conversion fails.
///
/// # Example
///
/// ```typescript
/// const floatResult = Float.fromFixedDecimalLossy("12345", 2);
/// if (floatResult.error) {
/// console.error(floatResult.error);
/// }
/// const float = floatResult.value;
/// assert(float.format() === "123.45");
/// ```
#[wasm_export(js_name = "fromFixedDecimalLossy", preserve_js_class)]
pub fn from_fixed_decimal_lossy_js(value: BigInt, decimals: u8) -> Result<Float, FloatError> {
let value_str: String = value.to_string(10)?.into();
let val = U256::from_str(&value_str)?;
Self::from_fixed_decimal_lossy(val, decimals)
}

/// Converts a `Float` to a fixed-point decimal value using the specified number of decimals lossy.
///
/// # Arguments
///
/// * `decimals` - The number of decimals in the fixed-point representation.
///
/// # Returns
///
/// * `Ok(String)` - The resulting fixed-point decimal value as a string.
/// * `Err(FloatError)` - If the conversion fails.
///
/// # Example
///
/// ```typescript
/// const float = Float.fromFixedDecimal(12345n, 3).value!;
/// const result = float.toFixedDecimalLossy(2);
/// if (result.error) {
/// console.error(result.error);
/// }
/// assert(result.value === "1234");
/// ```
/// ToFixedDecimalLossyResult containing the value and lossless flag.
#[wasm_export(
js_name = "toFixedDecimalLossy",
preserve_js_class,
unchecked_return_type = "bigint"
unchecked_return_type = "ToFixedDecimalLossyResult"
)]
pub fn to_fixed_decimal_lossy_js(&self, decimals: u8) -> Result<BigInt, FloatError> {
let fixed = self.to_fixed_decimal_lossy(decimals)?;
BigInt::from_str(&fixed.to_string())
.map_err(|e| FloatError::JsSysError(e.to_string().into()))
pub fn to_fixed_decimal_lossy_js(
&self,
decimals: u8,
) -> Result<ToFixedDecimalLossyResult, FloatError> {
let (fixed, lossless) = self.to_fixed_decimal_lossy(decimals)?;
Ok(ToFixedDecimalLossyResult {
value: fixed.to_string(),
lossless,
})
}

/// Parses a decimal string into a `Float`.
Expand Down
99 changes: 80 additions & 19 deletions crates/float/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ impl Float {
///
/// # Returns
///
/// * `Ok(Float)` - The resulting `Float` value.
/// * `Ok((Float, bool))` - The resulting `Float` value and a boolean indicating if the conversion was lossless.
/// * `Err(FloatError)` - If the conversion fails.
///
/// # Example
Expand All @@ -144,18 +144,19 @@ impl Float {
/// // 123.45 with 2 decimals is represented as 12345
/// let value = U256::from(12345u64);
/// let decimals = 2u8;
/// let float = Float::from_fixed_decimal_lossy(value, decimals)?;
/// let (float, lossless) = Float::from_fixed_decimal_lossy(value, decimals)?;
/// assert_eq!(float.format()?, "123.45");
/// assert!(lossless);
///
/// anyhow::Ok(())
/// ```
pub fn from_fixed_decimal_lossy(value: U256, decimals: u8) -> Result<Self, FloatError> {
pub fn from_fixed_decimal_lossy(value: U256, decimals: u8) -> Result<(Self, bool), FloatError> {
let calldata = DecimalFloat::fromFixedDecimalLossyCall { value, decimals }.abi_encode();

execute_call(Bytes::from(calldata), |output| {
let decoded =
DecimalFloat::fromFixedDecimalLossyCall::abi_decode_returns(output.as_ref())?;
Ok(Float(decoded._0))
Ok((Float(decoded._0), decoded._1))
})
}

Expand All @@ -167,7 +168,7 @@ impl Float {
///
/// # Returns
///
/// * `Ok(U256)` - The resulting fixed-point decimal value.
/// * `Ok((U256, bool))` - The resulting fixed-point decimal value and a boolean indicating if the conversion was lossless.
/// * `Err(FloatError)` - If the conversion fails.
///
/// # Example
Expand All @@ -178,19 +179,20 @@ impl Float {
///
/// // 123.45 with 2 decimals becomes 12345
/// let float = Float::from_fixed_decimal(U256::from(12345), 3)?;
/// let fixed = float.to_fixed_decimal_lossy(2)?;
/// let (fixed, lossless) = float.to_fixed_decimal_lossy(2)?;
/// assert_eq!(fixed, U256::from(1234u64));
/// assert!(!lossless);
///
/// anyhow::Ok(())
/// ```
pub fn to_fixed_decimal_lossy(self, decimals: u8) -> Result<U256, FloatError> {
pub fn to_fixed_decimal_lossy(self, decimals: u8) -> Result<(U256, bool), FloatError> {
let Float(float) = self;
let calldata = DecimalFloat::toFixedDecimalLossyCall { float, decimals }.abi_encode();

execute_call(Bytes::from(calldata), |output| {
let decoded =
DecimalFloat::toFixedDecimalLossyCall::abi_decode_returns(output.as_ref())?;
Ok(decoded._0)
Ok((decoded._0, decoded._1))
})
}

Expand Down Expand Up @@ -2001,7 +2003,8 @@ mod tests {

#[test]
fn test_from_fixed_decimal_lossy() {
let cases = vec![
// Test lossless conversions (values that fit in Float's precision)
let lossless_cases = vec![
(U256::from(0u128), 0u8, "0"),
(U256::from(0u128), 18u8, "0"),
(U256::from(1u128), 18u8, "1e-18"),
Expand All @@ -2010,27 +2013,74 @@ mod tests {
(U256::from(1000000000000000000u128), 18u8, "1"),
];

for (amount, decimals, expected) in cases {
let float = Float::from_fixed_decimal_lossy(amount, decimals).expect("should convert");
for (amount, decimals, expected) in lossless_cases {
let (float, lossless) =
Float::from_fixed_decimal_lossy(amount, decimals).expect("should convert");
let expected = Float::parse(expected.to_string()).unwrap();
assert!(float.eq(expected).unwrap());
assert!(
lossless,
"conversion should be lossless for amount={}, decimals={}",
amount, decimals
);
}

// Test lossy conversion with U256::MAX (too large to fit in Float's 224-bit coefficient)
let (float, lossless) = Float::from_fixed_decimal_lossy(U256::MAX, 1).unwrap();
assert!(!lossless, "U256::MAX conversion should be lossy");
assert!(!float.is_zero().unwrap(), "result should not be zero");
}

#[test]
fn test_to_fixed_decimal_lossy() {
let cases = vec![
(U256::from(0), 0u8, 0u128),
(U256::from(0), 18u8, 0u128),
// Test lossy conversions (loss of precision)
let lossy_cases = vec![
(U256::from(1), 18u8, 0u128),
(U256::from(123456789), 0u8, 12345678u128),
(U256::from(123456789), 2u8, 12345678u128),
];

for (input, decimals, expected) in cases {
for (input, decimals, expected) in lossy_cases {
let float = Float::from_fixed_decimal(input, decimals + 1).unwrap();
let fixed = float.to_fixed_decimal_lossy(decimals).unwrap();
assert_eq!(fixed, U256::from(expected));
let (fixed, lossless) = float.to_fixed_decimal_lossy(decimals).unwrap();
assert_eq!(
fixed,
U256::from(expected),
"wrong value for input={}, decimals={}",
input,
decimals
);
assert!(
!lossless,
"should be lossy for input={}, decimals={}",
input, decimals
);
}

// Test lossless conversions (no loss of precision)
let lossless_cases = vec![
// Zero is always lossless
(U256::from(0), 0u8, 0u128),
(U256::from(0), 18u8, 0u128),
// Converting 12340 with 3 decimals (12.340) to 2 decimals (12.34) is lossless
(U256::from(12340), 3u8, 1234u128),
];

for (input, decimals, expected) in lossless_cases {
let float = Float::from_fixed_decimal(input, decimals + 1).unwrap();
let (fixed, lossless) = float.to_fixed_decimal_lossy(decimals).unwrap();
assert_eq!(
fixed,
U256::from(expected),
"wrong value for input={}, decimals={}",
input,
decimals
);
assert!(
lossless,
"should be lossless for input={}, decimals={}",
input, decimals
);
}
}

Expand All @@ -2042,12 +2092,23 @@ mod tests {
let exponent = -(decimals as i32 + 1);
let value = U256::from(coeff);

let float = Float::from_fixed_decimal_lossy(value, decimals + 1).unwrap();
let (float, from_lossless) = Float::from_fixed_decimal_lossy(value, decimals + 1).unwrap();
let expected = Float::pack_lossless(coeff, exponent).unwrap();
prop_assert!(float.eq(expected).unwrap());

let fixed = float.to_fixed_decimal_lossy(decimals).unwrap();
// from_fixed_decimal_lossy should be lossless for values that fit in Float's precision
prop_assert!(from_lossless, "from_fixed_decimal_lossy should be lossless for coeff={coeff}");

let (fixed, to_lossless) = float.to_fixed_decimal_lossy(decimals).unwrap();
assert_eq!(fixed, value / U256::from(10));

// Converting from decimals+1 to decimals should be lossy unless the value is zero or
// the last digit is zero (divisible by 10)
if value == U256::ZERO || value % U256::from(10) == U256::ZERO {
prop_assert!(to_lossless, "to_fixed_decimal_lossy should be lossless when last digit is 0: value={}", value);
} else {
prop_assert!(!to_lossless, "to_fixed_decimal_lossy should be lossy when losing precision: value={}", value);
}
}
}

Expand Down
22 changes: 22 additions & 0 deletions test_js/float.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,28 @@ describe('Test Float Bindings', () => {
expect(result).toBe(originalValue);
});

it('should test fromFixedDecimalLossy with lossless conversion', () => {
const result = Float.fromFixedDecimalLossy(12345n, 2);
expect(result.float.format()?.value!).toBe('123.45');
expect(result.lossless).toBe(true);
});

it('should test toFixedDecimalLossy with lossy conversion', () => {
const float = Float.fromFixedDecimal(12345n, 3)?.value!;
const result = float.toFixedDecimalLossy(2);
expect(result.error).toBeUndefined();
expect(result.value!.value).toBe('1234');
expect(result.value!.lossless).toBe(false);
});

it('should test toFixedDecimalLossy with lossless conversion', () => {
const float = Float.fromFixedDecimal(12340n, 3)?.value!;
const result = float.toFixedDecimalLossy(2);
expect(result.error).toBeUndefined();
expect(result.value!.value).toBe('1234');
expect(result.value!.lossless).toBe(true);
});


it('should try from bigint', () => {
const result = Float.tryFromBigint(5n);
Expand Down