use std::time::{SystemTime, UNIX_EPOCH};
use alloy_primitives::{Address, B256, I256, U256};
use rust_decimal::Decimal;
use thiserror::Error;
use crate::common::consts::DECIMAL_SCALE;
#[derive(Debug, Error, PartialEq, Eq)]
pub enum HexConstError {
#[error(
"{name} is a placeholder; replace with the value from Protocol Constants at https://docs.derive.xyz before signing"
)]
Placeholder {
name: &'static str,
},
#[error("{name} is not valid {kind} hex: {message}")]
InvalidHex {
name: &'static str,
kind: &'static str,
message: String,
},
}
pub fn utc_now_ms() -> Result<u64, std::time::SystemTimeError> {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
}
pub fn decimal_to_scaled_i256(value: Decimal) -> Result<I256, &'static str> {
let scaled = value
.checked_mul(Decimal::from(DECIMAL_SCALE))
.ok_or("decimal scaling overflow before truncation")?;
let truncated = scaled.trunc();
let mantissa_str = truncated.to_string();
I256::from_dec_str(&mantissa_str).map_err(|_| "scaled decimal exceeds signed 256-bit range")
}
pub fn decimal_to_scaled_u256(value: Decimal) -> Result<U256, &'static str> {
if value.is_sign_negative() {
return Err("unsigned scaled decimal must be non-negative");
}
let scaled = value
.checked_mul(Decimal::from(DECIMAL_SCALE))
.ok_or("decimal scaling overflow before truncation")?;
let truncated = scaled.trunc();
let mantissa_str = truncated.to_string();
U256::from_str_radix(&mantissa_str, 10)
.map_err(|_| "scaled decimal exceeds unsigned 256-bit range")
}
pub fn parse_address_const(value: &str, name: &'static str) -> Result<Address, HexConstError> {
if value.contains("<paste_") {
return Err(HexConstError::Placeholder { name });
}
value
.parse::<Address>()
.map_err(|e| HexConstError::InvalidHex {
name,
kind: "20-byte address",
message: e.to_string(),
})
}
pub fn parse_b256_const(value: &str, name: &'static str) -> Result<B256, HexConstError> {
if value.contains("<paste_") {
return Err(HexConstError::Placeholder { name });
}
value
.parse::<B256>()
.map_err(|e| HexConstError::InvalidHex {
name,
kind: "32-byte hash",
message: e.to_string(),
})
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use rust_decimal_macros::dec;
use super::*;
#[rstest]
fn test_decimal_to_scaled_i256_one_unit() {
let scaled = decimal_to_scaled_i256(dec!(1)).unwrap();
assert_eq!(scaled, I256::try_from(DECIMAL_SCALE).unwrap());
}
#[rstest]
fn test_decimal_to_scaled_i256_handles_fractional() {
let scaled = decimal_to_scaled_i256(dec!(0.5)).unwrap();
let expected = I256::try_from(DECIMAL_SCALE / 2).unwrap();
assert_eq!(scaled, expected);
}
#[rstest]
fn test_decimal_to_scaled_i256_handles_negative() {
let scaled = decimal_to_scaled_i256(dec!(-2)).unwrap();
let expected = I256::try_from(DECIMAL_SCALE).unwrap() * I256::try_from(-2).unwrap();
assert_eq!(scaled, expected);
}
#[rstest]
fn test_decimal_to_scaled_u256_one_unit() {
let scaled = decimal_to_scaled_u256(dec!(1)).unwrap();
assert_eq!(scaled, U256::from(DECIMAL_SCALE));
}
#[rstest]
fn test_decimal_to_scaled_u256_rejects_negative() {
let err = decimal_to_scaled_u256(dec!(-1)).expect_err("must reject negative");
assert!(err.contains("non-negative"));
}
#[rstest]
fn test_parse_address_const_rejects_placeholder() {
let err = parse_address_const(
"0x<paste_from_docs.derive.xyz_protocol_constants>",
"TRADE_MODULE_ADDRESS_MAINNET",
)
.expect_err("must reject placeholder");
assert_eq!(
err,
HexConstError::Placeholder {
name: "TRADE_MODULE_ADDRESS_MAINNET"
}
);
}
#[rstest]
fn test_parse_address_const_accepts_valid_hex() {
let addr =
parse_address_const("0x0000000000000000000000000000000000001234", "TEST").unwrap();
assert_eq!(
format!("{addr:?}"),
"0x0000000000000000000000000000000000001234"
);
}
#[rstest]
fn test_parse_b256_const_rejects_placeholder() {
let err = parse_b256_const(
"0x<paste_from_docs.derive.xyz_protocol_constants>",
"ACTION_TYPEHASH",
)
.expect_err("must reject placeholder");
assert_eq!(
err,
HexConstError::Placeholder {
name: "ACTION_TYPEHASH"
}
);
}
#[rstest]
fn test_parse_b256_const_accepts_valid_hex() {
let value = "0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
let hash = parse_b256_const(value, "TEST").unwrap();
assert_eq!(hash.0[0], 0x00);
assert_eq!(hash.0[31], 0x1f);
}
#[rstest]
fn test_utc_now_ms_returns_thirteen_digit_value() {
let now = utc_now_ms().unwrap();
assert!(now > 1_700_000_000_000, "ms timestamp too small: {now}");
}
}