1use std::fmt;
2use std::str::FromStr;
3
4use alloy::{
5 primitives::{Address, FixedBytes, I256, U256},
6 signers::Signer,
7};
8use sonic_rs::{JsonValueTrait, Value};
9
10use super::{
11 hypercall_api_domain, hypercall_manager_domain, hypercall_rsm_domain, ActionKey,
12 CancelOrderByCloid, CancelOrderByOid, CreditOption, CreditToken, HCUpdateApiWallet, HLCancel,
13 HLCancelByCloid, HLOrder, HLSendAsset, LimitOrder, SendAsset, StartLiquidation,
14 StopLiquidation, SystemCreditOption, SystemCreditToken, SystemStartLiquidation,
15 SystemStopLiquidation, SystemWithdrawToken, UpdateApiWallet, WithdrawToken,
16};
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct DirectiveSigningError {
20 message: String,
21}
22
23impl DirectiveSigningError {
24 fn new(message: impl Into<String>) -> Self {
25 Self {
26 message: message.into(),
27 }
28 }
29}
30
31impl fmt::Display for DirectiveSigningError {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 write!(f, "{}", self.message)
34 }
35}
36
37impl std::error::Error for DirectiveSigningError {}
38
39fn parse_uint_field(action: &Value, field: &str) -> Result<u128, DirectiveSigningError> {
40 let value = action
41 .get(field)
42 .ok_or_else(|| DirectiveSigningError::new(format!("Missing action field '{}'", field)))?;
43
44 if value.is_number() {
45 value
46 .as_u64()
47 .map(u128::from)
48 .ok_or_else(|| DirectiveSigningError::new(format!("Invalid integer in '{}'", field)))
49 } else if value.is_str() {
50 let s = value.as_str().unwrap();
51 if s.is_empty() || !s.as_bytes().iter().all(|b| b.is_ascii_digit()) {
52 return Err(DirectiveSigningError::new(format!(
53 "Invalid decimal string in '{}'",
54 field
55 )));
56 }
57 s.parse::<u128>().map_err(|e| {
58 DirectiveSigningError::new(format!("Invalid numeric range in '{}': {}", field, e))
59 })
60 } else {
61 Err(DirectiveSigningError::new(format!(
62 "Field '{}' must be number or decimal string",
63 field
64 )))
65 }
66}
67
68fn parse_u256_field(action: &Value, field: &str) -> Result<U256, DirectiveSigningError> {
69 let value = action
70 .get(field)
71 .ok_or_else(|| DirectiveSigningError::new(format!("Missing action field '{}'", field)))?;
72
73 if value.is_number() {
74 value
75 .as_u64()
76 .map(U256::from)
77 .ok_or_else(|| DirectiveSigningError::new(format!("Invalid integer in '{}'", field)))
78 } else if value.is_str() {
79 let s = value.as_str().unwrap();
80 if s.is_empty() || !s.as_bytes().iter().all(|b| b.is_ascii_digit()) {
81 return Err(DirectiveSigningError::new(format!(
82 "Invalid decimal string in '{}'",
83 field
84 )));
85 }
86 U256::from_str(s).map_err(|e| {
87 DirectiveSigningError::new(format!("Invalid numeric range in '{}': {}", field, e))
88 })
89 } else {
90 Err(DirectiveSigningError::new(format!(
91 "Field '{}' must be number or decimal string",
92 field
93 )))
94 }
95}
96
97fn parse_bool_field(action: &Value, field: &str) -> Result<bool, DirectiveSigningError> {
98 action.get(field).and_then(|v| v.as_bool()).ok_or_else(|| {
99 DirectiveSigningError::new(format!("Missing boolean action field '{}'", field))
100 })
101}
102
103fn parse_address_field(action: &Value, field: &str) -> Result<Address, DirectiveSigningError> {
104 let value = action.get(field).and_then(|v| v.as_str()).ok_or_else(|| {
105 DirectiveSigningError::new(format!("Missing address action field '{}'", field))
106 })?;
107 Address::from_str(value)
108 .map_err(|e| DirectiveSigningError::new(format!("Invalid address in '{}': {}", field, e)))
109}
110
111fn parse_int_field(action: &Value, field: &str) -> Result<i128, DirectiveSigningError> {
112 let value = action
113 .get(field)
114 .ok_or_else(|| DirectiveSigningError::new(format!("Missing action field '{}'", field)))?;
115
116 if value.is_i64() {
117 value
118 .as_i64()
119 .map(i128::from)
120 .ok_or_else(|| DirectiveSigningError::new(format!("Invalid integer in '{}'", field)))
121 } else if value.is_str() {
122 value.as_str().unwrap().parse::<i128>().map_err(|e| {
123 DirectiveSigningError::new(format!("Invalid numeric range in '{}': {}", field, e))
124 })
125 } else {
126 Err(DirectiveSigningError::new(format!(
127 "Field '{}' must be signed integer or decimal string",
128 field
129 )))
130 }
131}
132
133fn parse_bytes32_field(action: &Value, field: &str) -> Result<[u8; 32], DirectiveSigningError> {
134 let value = action.get(field).and_then(|v| v.as_str()).ok_or_else(|| {
135 DirectiveSigningError::new(format!("Missing bytes32 action field '{}'", field))
136 })?;
137 let hex_str = value.strip_prefix("0x").ok_or_else(|| {
138 DirectiveSigningError::new(format!("Field '{}' must be 0x-prefixed hex", field))
139 })?;
140 if hex_str.len() != 64 {
141 return Err(DirectiveSigningError::new(format!(
142 "Field '{}' must be 32 bytes (64 hex chars)",
143 field
144 )));
145 }
146 let bytes = hex::decode(hex_str).map_err(|e| {
147 DirectiveSigningError::new(format!("Invalid bytes32 in '{}': {}", field, e))
148 })?;
149 bytes.try_into().map_err(|_| {
150 DirectiveSigningError::new(format!("Field '{}' must decode to exactly 32 bytes", field))
151 })
152}
153
154fn normalize_signature(signature: impl fmt::Display) -> String {
155 let sig = format!("{}", signature);
156 if sig.starts_with("0x") {
157 sig
158 } else {
159 format!("0x{}", sig)
160 }
161}
162
163pub async fn sign_directive_with_signer<S>(
164 signer: &S,
165 action_key: ActionKey,
166 account: Address,
167 nonce: u64,
168 action: &Value,
169 chain_id: u64,
170) -> Result<String, DirectiveSigningError>
171where
172 S: Signer + Sync,
173{
174 match action_key {
175 ActionKey::HlLimitOrder | ActionKey::RsmHlLimitOrder => {
176 let asset = parse_uint_field(action, "asset")?;
177 let is_buy = parse_bool_field(action, "isBuy")?;
178 let limit_px = parse_uint_field(action, "limitPx")?;
179 let sz = parse_uint_field(action, "sz")?;
180 let reduce_only = parse_bool_field(action, "reduceOnly")?;
181 let encoded_tif = parse_uint_field(action, "encodedTif")?;
182 let cloid = parse_uint_field(action, "cloid")?;
183
184 let encoded_tif_u8 = u8::try_from(encoded_tif).map_err(|e| {
185 DirectiveSigningError::new(format!("Invalid encodedTif range: {}", e))
186 })?;
187 if !matches!(encoded_tif_u8, 1..=3) {
188 return Err(DirectiveSigningError::new(
189 "encodedTif must be one of 1, 2, or 3",
190 ));
191 }
192
193 let message = HLOrder {
194 account,
195 nonce,
196 action: LimitOrder {
197 asset: u32::try_from(asset).map_err(|e| {
198 DirectiveSigningError::new(format!("Invalid asset range: {}", e))
199 })?,
200 isBuy: is_buy,
201 limitPx: u64::try_from(limit_px).map_err(|e| {
202 DirectiveSigningError::new(format!("Invalid limitPx range: {}", e))
203 })?,
204 sz: u64::try_from(sz).map_err(|e| {
205 DirectiveSigningError::new(format!("Invalid sz range: {}", e))
206 })?,
207 reduceOnly: reduce_only,
208 encodedTif: encoded_tif_u8,
209 cloid,
210 },
211 };
212
213 let domain = match action_key {
214 ActionKey::HlLimitOrder => hypercall_api_domain(chain_id).map_err(|e| {
215 DirectiveSigningError::new(format!("Invalid API signing domain: {}", e))
216 })?,
217 ActionKey::RsmHlLimitOrder => hypercall_rsm_domain(chain_id).map_err(|e| {
218 DirectiveSigningError::new(format!("Invalid RSM signing domain: {}", e))
219 })?,
220 _ => unreachable!(),
221 };
222 let signature = signer
223 .sign_typed_data(&message, &domain)
224 .await
225 .map_err(|e| {
226 DirectiveSigningError::new(format!("Failed to sign HLOrder: {}", e))
227 })?;
228 Ok(normalize_signature(signature))
229 }
230 ActionKey::HlCancelByOid | ActionKey::RsmHlCancelByOid => {
231 let asset = parse_uint_field(action, "asset")?;
232 let oid = parse_uint_field(action, "oid")?;
233
234 let message = HLCancel {
235 account,
236 nonce,
237 action: CancelOrderByOid {
238 asset: u32::try_from(asset).map_err(|e| {
239 DirectiveSigningError::new(format!("Invalid asset range: {}", e))
240 })?,
241 oid: u64::try_from(oid).map_err(|e| {
242 DirectiveSigningError::new(format!("Invalid oid range: {}", e))
243 })?,
244 },
245 };
246
247 let domain = match action_key {
248 ActionKey::HlCancelByOid => hypercall_api_domain(chain_id).map_err(|e| {
249 DirectiveSigningError::new(format!("Invalid API signing domain: {}", e))
250 })?,
251 ActionKey::RsmHlCancelByOid => hypercall_rsm_domain(chain_id).map_err(|e| {
252 DirectiveSigningError::new(format!("Invalid RSM signing domain: {}", e))
253 })?,
254 _ => unreachable!(),
255 };
256 let signature = signer
257 .sign_typed_data(&message, &domain)
258 .await
259 .map_err(|e| {
260 DirectiveSigningError::new(format!("Failed to sign HLCancel: {}", e))
261 })?;
262 Ok(normalize_signature(signature))
263 }
264 ActionKey::HlCancelByCloid | ActionKey::RsmHlCancelByCloid => {
265 let asset = parse_uint_field(action, "asset")?;
266 let cloid = parse_uint_field(action, "cloid")?;
267
268 let message = HLCancelByCloid {
269 account,
270 nonce,
271 action: CancelOrderByCloid {
272 asset: u32::try_from(asset).map_err(|e| {
273 DirectiveSigningError::new(format!("Invalid asset range: {}", e))
274 })?,
275 cloid,
276 },
277 };
278
279 let domain = match action_key {
280 ActionKey::HlCancelByCloid => hypercall_api_domain(chain_id).map_err(|e| {
281 DirectiveSigningError::new(format!("Invalid API signing domain: {}", e))
282 })?,
283 ActionKey::RsmHlCancelByCloid => hypercall_rsm_domain(chain_id).map_err(|e| {
284 DirectiveSigningError::new(format!("Invalid RSM signing domain: {}", e))
285 })?,
286 _ => unreachable!(),
287 };
288 let signature = signer
289 .sign_typed_data(&message, &domain)
290 .await
291 .map_err(|e| {
292 DirectiveSigningError::new(format!("Failed to sign HLCancelByCloid: {}", e))
293 })?;
294 Ok(normalize_signature(signature))
295 }
296 ActionKey::HcUpdateApiWallet => {
297 let name = parse_bytes32_field(action, "name")?;
298 let addr = parse_address_field(action, "addr")?;
299
300 let message = HCUpdateApiWallet {
301 account,
302 nonce,
303 action: UpdateApiWallet {
304 name: name.into(),
305 addr,
306 },
307 };
308
309 let domain = hypercall_manager_domain(chain_id).map_err(|e| {
310 DirectiveSigningError::new(format!("Invalid manager signing domain: {}", e))
311 })?;
312 let signature = signer
313 .sign_typed_data(&message, &domain)
314 .await
315 .map_err(|e| {
316 DirectiveSigningError::new(format!("Failed to sign HCUpdateApiWallet: {}", e))
317 })?;
318 Ok(normalize_signature(signature))
319 }
320 ActionKey::HlSendAsset | ActionKey::RsmHlSendAsset => {
321 let destination = parse_address_field(action, "destination")?;
322 let sub_account = parse_address_field(action, "subAccount")?;
323 let src_dex = parse_uint_field(action, "srcDex")?;
324 let dst_dex = parse_uint_field(action, "dstDex")?;
325 let token = parse_uint_field(action, "token")?;
326 let amount_wei = parse_uint_field(action, "amountWei")?;
327
328 let message = HLSendAsset {
329 account,
330 nonce,
331 action: SendAsset {
332 destination,
333 subAccount: sub_account,
334 srcDex: u32::try_from(src_dex).map_err(|e| {
335 DirectiveSigningError::new(format!("Invalid srcDex range: {}", e))
336 })?,
337 dstDex: u32::try_from(dst_dex).map_err(|e| {
338 DirectiveSigningError::new(format!("Invalid dstDex range: {}", e))
339 })?,
340 token: u64::try_from(token).map_err(|e| {
341 DirectiveSigningError::new(format!("Invalid token range: {}", e))
342 })?,
343 amountWei: u64::try_from(amount_wei).map_err(|e| {
344 DirectiveSigningError::new(format!("Invalid amountWei range: {}", e))
345 })?,
346 },
347 };
348
349 let domain = match action_key {
350 ActionKey::HlSendAsset => hypercall_manager_domain(chain_id).map_err(|e| {
351 DirectiveSigningError::new(format!("Invalid manager signing domain: {}", e))
352 })?,
353 ActionKey::RsmHlSendAsset => hypercall_rsm_domain(chain_id).map_err(|e| {
354 DirectiveSigningError::new(format!("Invalid RSM signing domain: {}", e))
355 })?,
356 _ => unreachable!(),
357 };
358 let signature = signer
359 .sign_typed_data(&message, &domain)
360 .await
361 .map_err(|e| {
362 DirectiveSigningError::new(format!("Failed to sign HLSendAsset: {}", e))
363 })?;
364 Ok(normalize_signature(signature))
365 }
366 ActionKey::SystemCreditToken => {
367 let src_dex = parse_uint_field(action, "srcDex")?;
368 let dst_dex = parse_uint_field(action, "dstDex")?;
369 let token = parse_uint_field(action, "token")?;
370 let amount_wei = parse_uint_field(action, "amountWei")?;
371
372 let message = SystemCreditToken {
373 account,
374 nonce,
375 action: CreditToken {
376 srcDex: u32::try_from(src_dex).map_err(|e| {
377 DirectiveSigningError::new(format!("Invalid srcDex range: {}", e))
378 })?,
379 dstDex: u32::try_from(dst_dex).map_err(|e| {
380 DirectiveSigningError::new(format!("Invalid dstDex range: {}", e))
381 })?,
382 token: u64::try_from(token).map_err(|e| {
383 DirectiveSigningError::new(format!("Invalid token range: {}", e))
384 })?,
385 amountWei: u64::try_from(amount_wei).map_err(|e| {
386 DirectiveSigningError::new(format!("Invalid amountWei range: {}", e))
387 })?,
388 },
389 };
390
391 let domain = hypercall_rsm_domain(chain_id).map_err(|e| {
392 DirectiveSigningError::new(format!("Invalid RSM signing domain: {}", e))
393 })?;
394 let signature = signer
395 .sign_typed_data(&message, &domain)
396 .await
397 .map_err(|e| {
398 DirectiveSigningError::new(format!("Failed to sign SystemCreditToken: {}", e))
399 })?;
400 Ok(normalize_signature(signature))
401 }
402 ActionKey::SystemCreditOption => {
403 let underlying = parse_bytes32_field(action, "underlying")?;
404 let expiry = parse_u256_field(action, "expiry")?;
405 let strike = parse_u256_field(action, "strike")?;
406 let is_call = parse_bool_field(action, "isCall")?;
407 let amount_wei = parse_u256_field(action, "amountWei")?;
408
409 let message = SystemCreditOption {
410 account,
411 nonce,
412 action: CreditOption {
413 underlying: FixedBytes::<32>::from(underlying),
414 expiry,
415 strike,
416 isCall: is_call,
417 amountWei: amount_wei,
418 },
419 };
420
421 let domain = hypercall_rsm_domain(chain_id).map_err(|e| {
422 DirectiveSigningError::new(format!("Invalid RSM signing domain: {}", e))
423 })?;
424 let signature = signer
425 .sign_typed_data(&message, &domain)
426 .await
427 .map_err(|e| {
428 DirectiveSigningError::new(format!("Failed to sign SystemCreditOption: {}", e))
429 })?;
430 Ok(normalize_signature(signature))
431 }
432 ActionKey::SystemStartLiquidation => {
433 let equity = parse_int_field(action, "equity")?;
434 let margin_needed = parse_u256_field(action, "marginNeeded")?;
435
436 let message = SystemStartLiquidation {
437 account,
438 nonce,
439 action: StartLiquidation {
440 equity: I256::try_from(equity).map_err(|e| {
441 DirectiveSigningError::new(format!("Invalid equity range: {}", e))
442 })?,
443 marginNeeded: margin_needed,
444 },
445 };
446
447 let domain = hypercall_rsm_domain(chain_id).map_err(|e| {
448 DirectiveSigningError::new(format!("Invalid RSM signing domain: {}", e))
449 })?;
450 let signature = signer
451 .sign_typed_data(&message, &domain)
452 .await
453 .map_err(|e| {
454 DirectiveSigningError::new(format!(
455 "Failed to sign SystemStartLiquidation: {}",
456 e
457 ))
458 })?;
459 Ok(normalize_signature(signature))
460 }
461 ActionKey::SystemStopLiquidation => {
462 let start_time = parse_u256_field(action, "startTime")?;
463
464 let message = SystemStopLiquidation {
465 account,
466 nonce,
467 action: StopLiquidation {
468 startTime: start_time,
469 },
470 };
471
472 let domain = hypercall_rsm_domain(chain_id).map_err(|e| {
473 DirectiveSigningError::new(format!("Invalid RSM signing domain: {}", e))
474 })?;
475 let signature = signer
476 .sign_typed_data(&message, &domain)
477 .await
478 .map_err(|e| {
479 DirectiveSigningError::new(format!(
480 "Failed to sign SystemStopLiquidation: {}",
481 e
482 ))
483 })?;
484 Ok(normalize_signature(signature))
485 }
486 ActionKey::SystemWithdrawToken => {
487 let destination = parse_address_field(action, "destination")?;
488 let sub_account = parse_address_field(action, "subAccount")?;
489 let src_dex = parse_uint_field(action, "srcDex")?;
490 let dst_dex = parse_uint_field(action, "dstDex")?;
491 let token = parse_uint_field(action, "token")?;
492 let amount_wei = parse_uint_field(action, "amountWei")?;
493
494 let message = SystemWithdrawToken {
495 account,
496 nonce,
497 action: WithdrawToken {
498 destination,
499 subAccount: sub_account,
500 srcDex: u32::try_from(src_dex).map_err(|e| {
501 DirectiveSigningError::new(format!("Invalid srcDex range: {}", e))
502 })?,
503 dstDex: u32::try_from(dst_dex).map_err(|e| {
504 DirectiveSigningError::new(format!("Invalid dstDex range: {}", e))
505 })?,
506 token: u64::try_from(token).map_err(|e| {
507 DirectiveSigningError::new(format!("Invalid token range: {}", e))
508 })?,
509 amountWei: u64::try_from(amount_wei).map_err(|e| {
510 DirectiveSigningError::new(format!("Invalid amountWei range: {}", e))
511 })?,
512 },
513 };
514
515 let domain = hypercall_rsm_domain(chain_id).map_err(|e| {
516 DirectiveSigningError::new(format!("Invalid RSM signing domain: {}", e))
517 })?;
518 let signature = signer
519 .sign_typed_data(&message, &domain)
520 .await
521 .map_err(|e| {
522 DirectiveSigningError::new(format!("Failed to sign SystemWithdrawToken: {}", e))
523 })?;
524 Ok(normalize_signature(signature))
525 }
526 ActionKey::HcTransferOption => Err(DirectiveSigningError::new(format!(
527 "Unsupported directive action key: {}",
528 action_key.as_str()
529 ))),
530 }
531}
532
533#[cfg(test)]
534mod tests {
535 use super::*;
536 use sonic_rs::json;
537
538 #[test]
539 fn parse_bool_field_accepts_boolean_values() {
540 let action = json!({
541 "isCall": true,
542 "reduceOnly": false,
543 });
544
545 assert!(parse_bool_field(&action, "isCall").unwrap());
546 assert!(!parse_bool_field(&action, "reduceOnly").unwrap());
547 }
548
549 #[test]
550 fn parse_bool_field_rejects_missing_or_non_boolean_values() {
551 let action = json!({
552 "isCall": "true",
553 "reduceOnly": 0,
554 });
555
556 assert_eq!(
557 parse_bool_field(&action, "missing")
558 .unwrap_err()
559 .to_string(),
560 "Missing boolean action field 'missing'"
561 );
562 assert_eq!(
563 parse_bool_field(&action, "isCall").unwrap_err().to_string(),
564 "Missing boolean action field 'isCall'"
565 );
566 assert_eq!(
567 parse_bool_field(&action, "reduceOnly")
568 .unwrap_err()
569 .to_string(),
570 "Missing boolean action field 'reduceOnly'"
571 );
572 }
573
574 #[test]
575 fn parse_u256_field_accepts_numbers_and_decimal_strings() {
576 let max_u256 = U256::MAX.to_string();
577 let action = json!({
578 "expiry": 1_778_361_600_u64,
579 "strike": "5000000000000",
580 "amountWei": max_u256,
581 });
582
583 assert_eq!(
584 parse_u256_field(&action, "expiry").unwrap(),
585 U256::from(1_778_361_600_u64)
586 );
587 assert_eq!(
588 parse_u256_field(&action, "strike").unwrap(),
589 U256::from(5_000_000_000_000_u64)
590 );
591 assert_eq!(parse_u256_field(&action, "amountWei").unwrap(), U256::MAX);
592 }
593
594 #[test]
595 fn parse_u256_field_rejects_missing_invalid_and_negative_values() {
596 let action = json!({
597 "empty": "",
598 "negative": "-1",
599 "alpha": "123abc",
600 "bool": true,
601 });
602
603 assert_eq!(
604 parse_u256_field(&action, "missing")
605 .unwrap_err()
606 .to_string(),
607 "Missing action field 'missing'"
608 );
609 assert_eq!(
610 parse_u256_field(&action, "empty").unwrap_err().to_string(),
611 "Invalid decimal string in 'empty'"
612 );
613 assert_eq!(
614 parse_u256_field(&action, "negative")
615 .unwrap_err()
616 .to_string(),
617 "Invalid decimal string in 'negative'"
618 );
619 assert_eq!(
620 parse_u256_field(&action, "alpha").unwrap_err().to_string(),
621 "Invalid decimal string in 'alpha'"
622 );
623 assert_eq!(
624 parse_u256_field(&action, "bool").unwrap_err().to_string(),
625 "Field 'bool' must be number or decimal string"
626 );
627 }
628}