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
85 changes: 85 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/float/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ homepage.workspace = true
alloy.workspace = true
thiserror.workspace = true
serde.workspace = true
wasm-bindgen-utils = "0.0.10"

[target.'cfg(not(target_family = "wasm"))'.dependencies]
revm = { workspace = true, default-features = false, features = [
Expand Down
2 changes: 2 additions & 0 deletions crates/float/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ pub enum FloatError {
DecimalFloatSelector(Result<DecimalFloatErrorSelector, FixedBytes<4>>),
#[error(transparent)]
Access(#[from] AccessError),
#[error("Invalid hex string: {0}")]
InvalidHex(String),
}

#[derive(Debug)]
Expand Down
111 changes: 106 additions & 5 deletions crates/float/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
use alloy::hex::FromHex;
use alloy::primitives::aliases::I224;
use alloy::primitives::{B256, Bytes};
use alloy::{sol, sol_types::SolCall};
use revm::primitives::{U256, fixed_bytes};
use serde::{Deserialize, Serialize};
use std::ops::{Add, Div, Mul, Neg, Sub};
use wasm_bindgen_utils::impl_wasm_traits;
use wasm_bindgen_utils::prelude::*;

pub mod error;
mod evm;
Expand All @@ -18,8 +21,9 @@ sol!(
"../../out/DecimalFloat.sol/DecimalFloat.json"
);

#[derive(Debug, Copy, Clone, Default, Serialize, Deserialize)]
pub struct Float(pub B256);
#[derive(Debug, Copy, Clone, Default, Serialize, Deserialize, Hash, Tsify)]
pub struct Float(#[tsify(type = "`0x${string}`")] pub B256);
impl_wasm_traits!(Float);

impl Float {
/// Converts a fixed-point decimal value to a `Float` using the specified number of decimals.
Expand Down Expand Up @@ -159,9 +163,51 @@ impl Float {
})
}

/// Returns the 32-byte hexadecimal string representation of the float.
///
/// # Returns
///
/// * `String` - The 32-byte hex string.
///
/// # Example
///
/// ```
/// use rain_math_float::Float;
/// let float = Float::from_hex("0x0000000000000000000000000000000000000000000000000000000000000005").unwrap();
/// assert_eq!(float.as_hex(), "0x0000000000000000000000000000000000000000000000000000000000000005");
/// ```
pub fn as_hex(self) -> String {
alloy::hex::encode_prefixed(self.0)
}

/// Constructs a `Float` from a 32-byte hexadecimal string.
///
/// # Arguments
///
/// * `hex` - The 32-byte hex string to parse.
///
/// # Returns
///
/// * `Ok(Float)` - The float parsed from the hex string.
/// * `Err(FloatError)` - If the hex string is not valid or not 32 bytes.
///
/// # Example
///
/// ```
/// use rain_math_float::Float;
/// let float = Float::from_hex("0x0000000000000000000000000000000000000000000000000000000000000005")?;
/// assert_eq!(float.as_hex(), "0x0000000000000000000000000000000000000000000000000000000000000005");
/// anyhow::Ok(())
/// ```
pub fn from_hex(hex: &str) -> Result<Self, FloatError> {
let bytes = B256::from_hex(hex).map_err(|_| FloatError::InvalidHex(hex.to_string()))?;
Ok(Float(bytes))
}

/// Formats the float as a decimal string.
///
/// NOTE: Uses 18 decimal places.
/// NOTE: Uses 18 decimal places and fails if the float has more than
/// that number of decimals.
///
/// # Returns
///
Expand All @@ -188,6 +234,32 @@ impl Float {
})
}

/// Formats the float as a decimal string. Gets truncated to 18 decimal
/// places if it has more than that.
///
/// # Returns
///
/// * `Ok(String)` - The formatted string.
/// * `Err(FloatError)` - If formatting fails.
///
/// # Example
///
/// ```
/// use rain_math_float::Float;
///
/// let float = Float::parse("2.5".to_string())?;
/// assert_eq!(float.format18()?, "2.5");
///
/// anyhow::Ok(())
/// ```
pub fn format18(self) -> Result<String, FloatError> {
let ten_to_18 = Self::from_fixed_decimal(U256::from(10u64).pow(U256::from(18)), 0)?;
let multiplied = (self * ten_to_18)?;
let floored = multiplied.floor()?;
let divided = (floored / ten_to_18)?;
divided.format()
}
Comment on lines +237 to +261
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Jul 11, 2025

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

LGTM: format18 implementation is mathematically sound.

The implementation correctly truncates to 18 decimal places using the multiply-floor-divide approach. The method provides the safer alternative to format as intended by the PR objective.

Consider caching the ten_to_18 constant to avoid recreating it on each call:

+    const TEN_TO_18: Float = Float(B256::from_be_bytes([/* pre-computed value */]));
+    
    pub fn format18(self) -> Result<String, FloatError> {
-        let ten_to_18 = Self::from_fixed_decimal(U256::from(10u64).pow(U256::from(18)), 0)?;
-        let multiplied = (self * ten_to_18)?;
+        let multiplied = (self * Self::TEN_TO_18)?;
        let floored = multiplied.floor()?;
-        let divided = (floored / ten_to_18)?;
+        let divided = (floored / Self::TEN_TO_18)?;
        divided.format()
    }

This would improve performance by avoiding the conversion calculation on each call.

📝 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
/// Formats the float as a decimal string. Gets truncated to 18 decimal
/// places if it has more than that.
///
/// # Returns
///
/// * `Ok(String)` - The formatted string.
/// * `Err(FloatError)` - If formatting fails.
///
/// # Example
///
/// ```
/// use rain_math_float::Float;
///
/// let float = Float::parse("2.5".to_string())?;
/// assert_eq!(float.format18()?, "2.5");
///
/// anyhow::Ok(())
/// ```
pub fn format18(self) -> Result<String, FloatError> {
let ten_to_18 = Self::from_fixed_decimal(U256::from(10u64).pow(U256::from(18)), 0)?;
let multiplied = (self * ten_to_18)?;
let floored = multiplied.floor()?;
let divided = (floored / ten_to_18)?;
divided.format()
}
/// Formats the float as a decimal string. Gets truncated to 18 decimal
/// places if it has more than that.
///
/// # Returns
///
/// * `Ok(String)` - The formatted string.
/// * `Err(FloatError)` - If formatting fails.
///
/// # Example
///
///
🤖 Prompt for AI Agents
In crates/float/src/lib.rs around lines 192 to 216, the format18 method
recreates the constant ten_to_18 on every call, which is inefficient. To fix
this, define ten_to_18 as a static or lazy-initialized constant outside the
method so it is computed only once and reused on subsequent calls, improving
performance by avoiding repeated calculations.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We don't need to optimize this for now

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@0xgleb Understood! No optimization needed at this time. The current implementation is clear and functional as-is.


🧠 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.

Learnt from: 0xgleb
PR: rainlanguage/rain.math.float#70
File: crates/float/src/evm.rs:38-43
Timestamp: 2025-07-03T11:20:50.456Z
Learning: In the rainlanguage/rain.math.float codebase, the user 0xgleb prefers not to add explanatory comments for well-established Rust idioms like the double `?` pattern, as these are self-explanatory to experienced Rust developers and don't need over-commenting.

Learnt from: 0xgleb
PR: rainlanguage/rain.math.float#58
File: src/concrete/DecimalFloat.sol:175-182
Timestamp: 2025-06-16T13:17:28.513Z
Learning: In the rainlanguage/rain.math.float codebase, there's an established naming convention where functions accepting a `Float` type parameter consistently use `float` as the parameter name, even though it shadows the type name. This pattern is used throughout `LibDecimalFloat.sol` and should be maintained for consistency in related contracts like `DecimalFloat.sol`.

Learnt from: 0xgleb
PR: rainlanguage/rain.math.float#59
File: crates/float/src/lib.rs:447-461
Timestamp: 2025-06-17T10:11:32.740Z
Learning: In the rainlanguage/rain.math.float repository, the user 0xgleb prefers using .unwrap() over ? in tests because it provides clearer stack traces showing the exact line where panics occurred, making debugging easier.

Learnt from: 0xgleb
PR: rainlanguage/rain.math.float#58
File: crates/float/src/lib.rs:201-232
Timestamp: 2025-06-16T13:14:38.431Z
Learning: In the rain.math.float Rust crate, the `execute_call` function already provides sufficient abstraction for EVM contract calls by handling execution boilerplate, error handling, and result processing. Individual methods like `lt`, `eq`, `gt` only need to handle their specific call encoding and result decoding, making further abstraction unnecessary.

Learnt from: 0xgleb
PR: rainlanguage/rain.math.float#59
File: crates/float/src/lib.rs:233-242
Timestamp: 2025-06-17T10:17:56.205Z
Learning: In the rainlanguage/rain.math.float repository, the maintainer 0xgleb prefers to handle documentation additions and improvements in separate issues rather than inline with feature PRs.

Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#30
File: test/src/lib/LibDecimalFloat.gt.t.sol:33-36
Timestamp: 2025-04-25T03:58:01.307Z
Learning: In the rain.math.float library, all values of `Float` (which is a type alias for bytes32) are considered valid and can be safely used with methods like gt(), lt(), or eq() without causing reverts.

Learnt from: 0xgleb
PR: rainlanguage/rain.math.float#46
File: crates/float/proptest-regressions/lib.txt:1-3
Timestamp: 2025-06-11T09:03:20.771Z
Learning: The file `crates/float/proptest-regressions/lib.txt` is automatically generated by proptest and should not be manually edited.

Learnt from: 0xgleb
PR: rainlanguage/rain.math.float#64
File: src/concrete/DecimalFloat.sol:0-0
Timestamp: 2025-06-17T10:02:01.394Z
Learning: In the rain.math.float project, the Float type is designed with a specific bit layout: 224-bit signed coefficient and 32-bit signed exponent, represented as 32 bytes total. The LibDecimalFloat.unpack function returns values that are guaranteed to fit within int224 and int32 ranges due to this internal representation, making explicit range checks before casting unnecessary.

Learnt from: 0xgleb
PR: rainlanguage/rain.math.float#64
File: src/concrete/DecimalFloat.sol:0-0
Timestamp: 2025-06-17T10:02:01.394Z
Learning: In the rain.math.float project, the Float type is designed with a specific bit layout: 224-bit signed coefficient and 32-bit signed exponent, represented as 32 bytes total. The LibDecimalFloat.unpack function returns values that are guaranteed to fit within int224 and int32 ranges due to this internal representation, making explicit range checks before casting unnecessary.


/// Returns `true` if `self` is less than `b`.
///
/// # Arguments
Expand Down Expand Up @@ -780,10 +852,39 @@ mod tests {

proptest! {
#[test]
fn test_parse_format(float in reasonable_float()) {
fn test_format_parse(float in reasonable_float()) {
let formatted = float.format().unwrap();
let parsed = Float::parse(formatted.clone()).unwrap();
prop_assert_eq!(float.0, parsed.0);
prop_assert!(float.eq(parsed).unwrap());
}
}

#[test]
fn test_format18() {
let float = Float::parse("1.234567890123456789".to_string()).unwrap();
let formatted = float.format18().unwrap();
assert_eq!(formatted, "1.234567890123456789");

let float = Float::parse("1.2345678901234567891".to_string()).unwrap();
let formatted = float.format18().unwrap();
assert_eq!(formatted, "1.234567890123456789");
}

proptest! {
#[test]
fn test_format18_parse(float in reasonable_float()) {
let formatted = float.format18().unwrap();
let parsed = Float::parse(formatted.clone()).unwrap();
prop_assert!(float.eq(parsed).unwrap());
}
}

proptest! {
#[test]
fn test_as_from_hex(float in arb_float()) {
let hex = float.as_hex();
let parsed = Float::from_hex(&hex).unwrap();
prop_assert_eq!(parsed.as_hex(), hex);
}
}

Expand Down
Loading