nautilus_derive/signing/
encoding.rs1use std::time::{SystemTime, UNIX_EPOCH};
19
20use alloy_primitives::{Address, B256, I256, U256};
21use rust_decimal::Decimal;
22use thiserror::Error;
23
24use crate::common::consts::DECIMAL_SCALE;
25
26#[derive(Debug, Error, PartialEq, Eq)]
28pub enum HexConstError {
29 #[error(
31 "{name} is a placeholder; replace with the value from Protocol Constants at https://docs.derive.xyz before signing"
32 )]
33 Placeholder {
34 name: &'static str,
36 },
37 #[error("{name} is not valid {kind} hex: {message}")]
39 InvalidHex {
40 name: &'static str,
42 kind: &'static str,
44 message: String,
46 },
47}
48
49pub fn utc_now_ms() -> Result<u64, std::time::SystemTimeError> {
56 SystemTime::now()
57 .duration_since(UNIX_EPOCH)
58 .map(|d| d.as_millis() as u64)
59}
60
61pub fn decimal_to_scaled_i256(value: Decimal) -> Result<I256, &'static str> {
74 let scaled = value
75 .checked_mul(Decimal::from(DECIMAL_SCALE))
76 .ok_or("decimal scaling overflow before truncation")?;
77 let truncated = scaled.trunc();
78 let mantissa_str = truncated.to_string();
79 I256::from_dec_str(&mantissa_str).map_err(|_| "scaled decimal exceeds signed 256-bit range")
80}
81
82pub fn decimal_to_scaled_u256(value: Decimal) -> Result<U256, &'static str> {
92 if value.is_sign_negative() {
93 return Err("unsigned scaled decimal must be non-negative");
94 }
95 let scaled = value
96 .checked_mul(Decimal::from(DECIMAL_SCALE))
97 .ok_or("decimal scaling overflow before truncation")?;
98 let truncated = scaled.trunc();
99 let mantissa_str = truncated.to_string();
100 U256::from_str_radix(&mantissa_str, 10)
101 .map_err(|_| "scaled decimal exceeds unsigned 256-bit range")
102}
103
104pub fn parse_address_const(value: &str, name: &'static str) -> Result<Address, HexConstError> {
113 if value.contains("<paste_") {
114 return Err(HexConstError::Placeholder { name });
115 }
116 value
117 .parse::<Address>()
118 .map_err(|e| HexConstError::InvalidHex {
119 name,
120 kind: "20-byte address",
121 message: e.to_string(),
122 })
123}
124
125pub fn parse_b256_const(value: &str, name: &'static str) -> Result<B256, HexConstError> {
134 if value.contains("<paste_") {
135 return Err(HexConstError::Placeholder { name });
136 }
137 value
138 .parse::<B256>()
139 .map_err(|e| HexConstError::InvalidHex {
140 name,
141 kind: "32-byte hash",
142 message: e.to_string(),
143 })
144}
145
146#[cfg(test)]
147mod tests {
148 use rstest::rstest;
149 use rust_decimal_macros::dec;
150
151 use super::*;
152
153 #[rstest]
154 fn test_decimal_to_scaled_i256_one_unit() {
155 let scaled = decimal_to_scaled_i256(dec!(1)).unwrap();
156 assert_eq!(scaled, I256::try_from(DECIMAL_SCALE).unwrap());
157 }
158
159 #[rstest]
160 fn test_decimal_to_scaled_i256_handles_fractional() {
161 let scaled = decimal_to_scaled_i256(dec!(0.5)).unwrap();
162 let expected = I256::try_from(DECIMAL_SCALE / 2).unwrap();
163 assert_eq!(scaled, expected);
164 }
165
166 #[rstest]
167 fn test_decimal_to_scaled_i256_handles_negative() {
168 let scaled = decimal_to_scaled_i256(dec!(-2)).unwrap();
169 let expected = I256::try_from(DECIMAL_SCALE).unwrap() * I256::try_from(-2).unwrap();
170 assert_eq!(scaled, expected);
171 }
172
173 #[rstest]
174 fn test_decimal_to_scaled_u256_one_unit() {
175 let scaled = decimal_to_scaled_u256(dec!(1)).unwrap();
176 assert_eq!(scaled, U256::from(DECIMAL_SCALE));
177 }
178
179 #[rstest]
180 fn test_decimal_to_scaled_u256_rejects_negative() {
181 let err = decimal_to_scaled_u256(dec!(-1)).expect_err("must reject negative");
182 assert!(err.contains("non-negative"));
183 }
184
185 #[rstest]
186 fn test_parse_address_const_rejects_placeholder() {
187 let err = parse_address_const(
188 "0x<paste_from_docs.derive.xyz_protocol_constants>",
189 "TRADE_MODULE_ADDRESS_MAINNET",
190 )
191 .expect_err("must reject placeholder");
192 assert_eq!(
193 err,
194 HexConstError::Placeholder {
195 name: "TRADE_MODULE_ADDRESS_MAINNET"
196 }
197 );
198 }
199
200 #[rstest]
201 fn test_parse_address_const_accepts_valid_hex() {
202 let addr =
203 parse_address_const("0x0000000000000000000000000000000000001234", "TEST").unwrap();
204 assert_eq!(
205 format!("{addr:?}"),
206 "0x0000000000000000000000000000000000001234"
207 );
208 }
209
210 #[rstest]
211 fn test_parse_b256_const_rejects_placeholder() {
212 let err = parse_b256_const(
213 "0x<paste_from_docs.derive.xyz_protocol_constants>",
214 "ACTION_TYPEHASH",
215 )
216 .expect_err("must reject placeholder");
217 assert_eq!(
218 err,
219 HexConstError::Placeholder {
220 name: "ACTION_TYPEHASH"
221 }
222 );
223 }
224
225 #[rstest]
226 fn test_parse_b256_const_accepts_valid_hex() {
227 let value = "0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
228 let hash = parse_b256_const(value, "TEST").unwrap();
229 assert_eq!(hash.0[0], 0x00);
230 assert_eq!(hash.0[31], 0x1f);
231 }
232
233 #[rstest]
234 fn test_utc_now_ms_returns_thirteen_digit_value() {
235 let now = utc_now_ms().unwrap();
236 assert!(now > 1_700_000_000_000, "ms timestamp too small: {now}");
238 }
239}