diff --git a/crates/float/src/js_api.rs b/crates/float/src/js_api.rs index a3c020f6..9d8446cd 100644 --- a/crates/float/src/js_api.rs +++ b/crates/float/src/js_api.rs @@ -143,6 +143,33 @@ impl Float { Self::from_fixed_decimal(val, decimals) } + /// Converts a `Float` to a fixed-point decimal value using the specified number of decimals. + /// + /// # 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.parse("123.45").value!; + /// const result = float.toFixedDecimal(2); + /// if (result.error) { + /// console.error(result.error); + /// } + /// assert(result.value === "12345"); + /// ``` + #[wasm_export(js_name = "toFixedDecimal")] + pub fn to_fixed_decimal_js(&self, decimals: u8) -> Result { + let fixed = self.to_fixed_decimal(decimals)?; + Ok(fixed.to_string()) + } + /// Packs a coefficient and exponent into a `Float` in a lossless manner. /// /// # Arguments diff --git a/crates/float/src/lib.rs b/crates/float/src/lib.rs index 1b468502..801dfd0b 100644 --- a/crates/float/src/lib.rs +++ b/crates/float/src/lib.rs @@ -79,6 +79,41 @@ impl Float { }) } + /// Converts a `Float` to a fixed-point decimal value using the specified number of decimals. + /// + /// # Arguments + /// + /// * `decimals` - The number of decimals in the fixed-point representation. + /// + /// # Returns + /// + /// * `Ok(U256)` - The resulting fixed-point decimal value. + /// * `Err(FloatError)` - If the conversion fails. + /// + /// # Example + /// + /// ``` + /// use rain_math_float::Float; + /// use alloy::primitives::U256; + /// + /// // 123.45 with 2 decimals becomes 12345 + /// let float = Float::parse("123.45".to_string())?; + /// let fixed = float.to_fixed_decimal(2)?; + /// assert_eq!(fixed, U256::from(12345u64)); + /// + /// anyhow::Ok(()) + /// ``` + pub fn to_fixed_decimal(self, decimals: u8) -> Result { + let Float(float) = self; + let calldata = DecimalFloat::toFixedDecimalLosslessCall { float, decimals }.abi_encode(); + + execute_call(Bytes::from(calldata), |output| { + let decoded = + DecimalFloat::toFixedDecimalLosslessCall::abi_decode_returns(output.as_ref())?; + Ok(decoded) + }) + } + /// Packs a coefficient and exponent into a `Float` in a lossless manner. /// /// # Arguments @@ -1304,6 +1339,24 @@ mod tests { )); } + #[test] + fn test_to_fixed_decimal() { + let cases = vec![ + ("0", 0u8, 0u128), + ("0", 18u8, 0u128), + ("1e-18", 18u8, 1u128), + ("123456789", 0u8, 123456789u128), + ("123456789e-2", 2u8, 123456789u128), + ("1", 18u8, 1000000000000000000u128), + ]; + + for (input, decimals, expected) in cases { + let float = Float::parse(input.to_string()).unwrap(); + let fixed = float.to_fixed_decimal(decimals).unwrap(); + assert_eq!(fixed, U256::from(expected)); + } + } + #[test] fn test_frac_and_floor_integers() { let int_float = Float::parse("12345".to_string()).unwrap(); @@ -1341,7 +1394,7 @@ mod tests { proptest! { #[test] - fn test_from_fixed_decimal_valid_range(coeff in any::(), decimals in 0u8..=66u8) { + fn test_from_to_fixed_decimal_valid_range(coeff in any::(), decimals in 0u8..=66u8) { prop_assume!(coeff >= I224::ZERO); let exponent = -(decimals as i32); @@ -1350,6 +1403,9 @@ mod tests { let float = Float::from_fixed_decimal(value, decimals).unwrap(); let expected = Float::pack_lossless(coeff, exponent).unwrap(); prop_assert!(float.eq(expected).unwrap()); + + let fixed = float.to_fixed_decimal(decimals).unwrap(); + assert_eq!(fixed, value); } } diff --git a/src/concrete/DecimalFloat.sol b/src/concrete/DecimalFloat.sol index 58a039c4..d43e31b8 100644 --- a/src/concrete/DecimalFloat.sol +++ b/src/concrete/DecimalFloat.sol @@ -191,4 +191,13 @@ contract DecimalFloat { function fromFixedDecimalLosslessPacked(uint256 value, uint8 decimals) external pure returns (Float) { return LibDecimalFloat.fromFixedDecimalLosslessPacked(value, decimals); } + + /// Exposes `LibDecimalFloat.toFixedDecimalLossless` for offchain use. + /// @param float The Float struct to convert. + /// @param decimals The number of decimals in the fixed point + /// representation. e.g. If 1e18 represents 1 this would be 18 decimals. + /// @return The fixed point decimal value as a uint256. + function toFixedDecimalLossless(Float float, uint8 decimals) external pure returns (uint256) { + return LibDecimalFloat.toFixedDecimalLossless(float, decimals); + } } diff --git a/test_js/float.test.ts b/test_js/float.test.ts index 5a751947..90f34a85 100644 --- a/test_js/float.test.ts +++ b/test_js/float.test.ts @@ -28,6 +28,19 @@ describe('Test Float Bindings', () => { expect(float.format18()?.value!).toBe('123.45'); }); + it('should test toFixedDecimal', () => { + const float = Float.parse('123.45')?.value!; + expect(float.toFixedDecimal(2)?.value!).toBe('12345'); + }); + + it('should test toFixedDecimal roundtrip', () => { + const originalValue = '9876543210'; + const decimals = 8; + const float = Float.fromFixedDecimal(originalValue, decimals)?.value!; + const result = float.toFixedDecimal(decimals)?.value!; + expect(result).toBe(originalValue); + }); + it('should test packLossless', () => { const float = Float.packLossless('314', -2)?.value!; expect(float.format()?.value!).toBe('3.14');