nautilus_derive/signing/
encoding.rs1use std::time::UNIX_EPOCH;
19
20use alloy_primitives::{Address, B256, I256, U256};
21use rust_decimal::Decimal;
22use thiserror::Error;
23
24use crate::common::consts::{DECIMAL_PRECISION, 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> {
55 nautilus_core::time::wall_clock_now()
56 .duration_since(UNIX_EPOCH)
57 .map(|d| d.as_millis() as u64)
58}
59
60pub fn decimal_to_scaled_i256(value: Decimal) -> Result<I256, &'static str> {
72 let magnitude = decimal_to_scaled_u256(value.abs())?;
73 let signed =
74 I256::try_from(magnitude).map_err(|_| "scaled decimal exceeds signed 256-bit range")?;
75 Ok(if value.is_sign_negative() {
76 -signed
77 } else {
78 signed
79 })
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
96 if value.scale() > DECIMAL_PRECISION {
97 return Err("financial input exceeds 12 fractional digits");
98 }
99
100 let mantissa = U256::from(value.mantissa().unsigned_abs());
101 let factor = U256::from(DECIMAL_SCALE / 10_u128.pow(value.scale()));
102 mantissa
103 .checked_mul(factor)
104 .ok_or("scaled decimal exceeds unsigned 256-bit range")
105}
106
107pub fn parse_address_const(value: &str, name: &'static str) -> Result<Address, HexConstError> {
115 if value.contains("<paste_") {
116 return Err(HexConstError::Placeholder { name });
117 }
118
119 value
120 .parse::<Address>()
121 .map_err(|e| HexConstError::InvalidHex {
122 name,
123 kind: "20-byte address",
124 message: e.to_string(),
125 })
126}
127
128pub fn parse_b256_const(value: &str, name: &'static str) -> Result<B256, HexConstError> {
136 if value.contains("<paste_") {
137 return Err(HexConstError::Placeholder { name });
138 }
139
140 value
141 .parse::<B256>()
142 .map_err(|e| HexConstError::InvalidHex {
143 name,
144 kind: "32-byte hash",
145 message: e.to_string(),
146 })
147}
148
149#[cfg(test)]
150mod tests {
151 use rstest::rstest;
152 use rust_decimal_macros::dec;
153
154 use super::*;
155
156 #[rstest]
157 fn test_decimal_to_scaled_i256_one_unit() {
158 let scaled = decimal_to_scaled_i256(dec!(1)).unwrap();
159 assert_eq!(scaled, I256::try_from(DECIMAL_SCALE).unwrap());
160 }
161
162 #[rstest]
163 fn test_decimal_to_scaled_i256_handles_fractional() {
164 let scaled = decimal_to_scaled_i256(dec!(0.5)).unwrap();
165 let expected = I256::try_from(DECIMAL_SCALE / 2).unwrap();
166 assert_eq!(scaled, expected);
167 }
168
169 #[rstest]
170 fn test_decimal_to_scaled_i256_handles_negative() {
171 let scaled = decimal_to_scaled_i256(dec!(-2)).unwrap();
172 let expected = I256::try_from(DECIMAL_SCALE).unwrap() * I256::try_from(-2).unwrap();
173 assert_eq!(scaled, expected);
174 }
175
176 #[rstest]
177 fn test_decimal_to_scaled_u256_one_unit() {
178 let scaled = decimal_to_scaled_u256(dec!(1)).unwrap();
179 assert_eq!(scaled, U256::from(DECIMAL_SCALE));
180 }
181
182 #[rstest]
183 fn test_decimal_to_scaled_u256_rejects_negative() {
184 let err = decimal_to_scaled_u256(dec!(-1)).expect_err("must reject negative");
185 assert!(err.contains("non-negative"));
186 }
187
188 #[rstest]
189 #[case(dec!(0.000000000001), 1_000_000_i128)]
190 #[case(dec!(-0.000000000001), -1_000_000_i128)]
191 #[case(dec!(1.123456789012), 1_123_456_789_012_000_000_i128)]
192 #[case(dec!(1.123456789000), 1_123_456_789_000_000_000_i128)]
193 fn test_decimal_precision_boundary(#[case] value: Decimal, #[case] expected: i128) {
194 assert_eq!(
195 decimal_to_scaled_i256(value),
196 Ok(I256::try_from(expected).unwrap())
197 );
198 }
199
200 #[rstest]
201 #[case(dec!(0.0000000000001))]
202 #[case(dec!(-0.0000000000001))]
203 #[case(dec!(1.1234567890123))]
204 #[case(dec!(1.1234567890120000))]
205 #[case(dec!(0.0000000000000))]
206 #[case(dec!(0.0000000000000000000000000001))]
207 fn test_decimal_rejects_excess_precision(#[case] value: Decimal) {
208 assert_eq!(
209 decimal_to_scaled_i256(value),
210 Err("financial input exceeds 12 fractional digits")
211 );
212 assert_eq!(
213 decimal_to_scaled_u256(value.abs()),
214 Err("financial input exceeds 12 fractional digits")
215 );
216 }
217
218 #[rstest]
219 fn test_decimal_max_scales_without_intermediate_decimal_overflow() {
220 let expected =
221 U256::from_str_radix("79228162514264337593543950335000000000000000000", 10).unwrap();
222 assert_eq!(decimal_to_scaled_u256(Decimal::MAX), Ok(expected));
223 assert_eq!(
224 decimal_to_scaled_i256(Decimal::MIN),
225 Ok(-I256::try_from(expected).unwrap())
226 );
227 }
228
229 #[rstest]
230 fn test_parse_address_const_rejects_placeholder() {
231 let err = parse_address_const(
232 "0x<paste_from_docs.derive.xyz_protocol_constants>",
233 "TRADE_MODULE_ADDRESS_MAINNET",
234 )
235 .expect_err("must reject placeholder");
236 assert_eq!(
237 err,
238 HexConstError::Placeholder {
239 name: "TRADE_MODULE_ADDRESS_MAINNET"
240 }
241 );
242 }
243
244 #[rstest]
245 fn test_parse_address_const_accepts_valid_hex() {
246 let addr =
247 parse_address_const("0x0000000000000000000000000000000000001234", "TEST").unwrap();
248 assert_eq!(
249 format!("{addr:?}"),
250 "0x0000000000000000000000000000000000001234"
251 );
252 }
253
254 #[rstest]
255 fn test_parse_b256_const_rejects_placeholder() {
256 let err = parse_b256_const(
257 "0x<paste_from_docs.derive.xyz_protocol_constants>",
258 "ACTION_TYPEHASH",
259 )
260 .expect_err("must reject placeholder");
261 assert_eq!(
262 err,
263 HexConstError::Placeholder {
264 name: "ACTION_TYPEHASH"
265 }
266 );
267 }
268
269 #[rstest]
270 fn test_parse_b256_const_accepts_valid_hex() {
271 let value = "0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
272 let hash = parse_b256_const(value, "TEST").unwrap();
273 assert_eq!(hash.0[0], 0x00);
274 assert_eq!(hash.0[31], 0x1f);
275 }
276
277 #[rstest]
278 fn test_utc_now_ms_returns_thirteen_digit_value() {
279 let now = utc_now_ms().unwrap();
280 assert!(now > 1_700_000_000_000, "ms timestamp too small: {now}");
282 }
283}