1use std::str::FromStr;
17
18use alloy_primitives::{Address, B256, keccak256};
19use alloy_signer::SignerSync;
20use alloy_signer_local::PrivateKeySigner;
21use alloy_sol_types::{SolStruct, eip712_domain};
22use serde::{Deserialize, Serialize};
23use serde_json::Value;
24
25use super::{nonce::TimeNonce, types::HyperliquidActionType};
26use crate::{
27 common::credential::EvmPrivateKey,
28 http::error::{Error, Result},
29};
30
31alloy_sol_types::sol! {
33 #[derive(Debug, Serialize, Deserialize)]
34 struct Agent {
35 string source;
36 bytes32 connectionId;
37 }
38}
39
40#[derive(Debug, Clone)]
42pub struct SignRequest {
43 pub action: Value, pub action_bytes: Option<Vec<u8>>, pub time_nonce: TimeNonce,
46 pub action_type: HyperliquidActionType,
47 pub is_testnet: bool,
48 pub vault_address: Option<String>,
49}
50
51#[derive(Debug, Clone)]
53pub struct SignatureBundle {
54 pub signature: String,
55}
56
57#[derive(Debug, Clone)]
59pub struct HyperliquidEip712Signer {
60 private_key: EvmPrivateKey,
61}
62
63impl HyperliquidEip712Signer {
64 pub fn new(private_key: EvmPrivateKey) -> Self {
65 Self { private_key }
66 }
67
68 pub fn sign(&self, request: &SignRequest) -> Result<SignatureBundle> {
69 let signature = match request.action_type {
70 HyperliquidActionType::L1 => self.sign_l1_action(request)?,
71 HyperliquidActionType::UserSigned => {
72 self.sign_user_signed_action(&request.action, request.time_nonce)?
73 }
74 };
75
76 Ok(SignatureBundle { signature })
77 }
78
79 pub fn sign_l1_action(&self, request: &SignRequest) -> Result<String> {
80 let connection_id = self.compute_connection_id(request)?;
89
90 let source = if request.is_testnet {
92 "b".to_string()
93 } else {
94 "a".to_string()
95 };
96
97 let agent = Agent {
98 source,
99 connectionId: connection_id,
100 };
101
102 let domain = eip712_domain! {
104 name: "Exchange",
105 version: "1",
106 chain_id: 1337,
107 verifying_contract: Address::ZERO,
108 };
109
110 let signing_hash = agent.eip712_signing_hash(&domain);
111
112 self.sign_hash(&signing_hash.0)
113 }
114
115 fn compute_connection_id(&self, request: &SignRequest) -> Result<B256> {
116 let mut bytes = if let Some(action_bytes) = &request.action_bytes {
117 action_bytes.clone()
118 } else {
119 log::warn!(
120 "Falling back to JSON Value msgpack serialization - this may cause hash mismatch!"
121 );
122 rmp_serde::to_vec_named(&request.action)
123 .map_err(|e| Error::transport(format!("Failed to serialize action: {e}")))?
124 };
125
126 let timestamp = request.time_nonce.as_millis() as u64;
128 bytes.extend_from_slice(×tamp.to_be_bytes());
129
130 if let Some(vault_addr) = &request.vault_address {
131 bytes.push(1); let vault_hex = vault_addr.trim_start_matches("0x");
133 let vault_bytes = hex::decode(vault_hex)
134 .map_err(|e| Error::transport(format!("Invalid vault address: {e}")))?;
135 bytes.extend_from_slice(&vault_bytes);
136 } else {
137 bytes.push(0); }
139
140 Ok(keccak256(&bytes))
141 }
142
143 pub fn sign_user_signed_action(&self, action: &Value, _nonce: TimeNonce) -> Result<String> {
144 let canonicalized = Self::canonicalize_action(action)?;
145
146 let domain_hash = self.get_domain_hash()?;
148 let action_hash = self.hash_typed_data(&canonicalized)?;
149 let message_hash = self.create_eip712_hash(&domain_hash, &action_hash)?;
150
151 self.sign_hash(&message_hash)
152 }
153
154 fn get_domain_hash(&self) -> Result<[u8; 32]> {
155 let domain_type_hash = keccak256(
158 b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)",
159 );
160
161 let name_hash = keccak256(b"Hyperliquid");
162 let version_hash = keccak256(b"1");
163
164 let chain_id: [u8; 32] = {
166 let mut bytes = [0u8; 32];
167 bytes[31] = 1; bytes
169 };
170
171 let verifying_contract = hex::decode("0000000000000000000000000000000000000000")
174 .map_err(|e| Error::transport(format!("Failed to decode verifying contract: {e}")))?;
175 let mut contract_bytes = [0u8; 32];
176 contract_bytes[12..].copy_from_slice(&verifying_contract);
177
178 let mut combined = Vec::with_capacity(160);
180 combined.extend_from_slice(domain_type_hash.as_slice());
181 combined.extend_from_slice(name_hash.as_slice());
182 combined.extend_from_slice(version_hash.as_slice());
183 combined.extend_from_slice(&chain_id);
184 combined.extend_from_slice(&contract_bytes);
185
186 Ok(*keccak256(&combined))
187 }
188
189 fn hash_typed_data(&self, data: &Value) -> Result<[u8; 32]> {
190 let json_str = serde_json::to_string(data)?;
193 Ok(*keccak256(json_str.as_bytes()))
194 }
195
196 fn create_eip712_hash(
197 &self,
198 domain_hash: &[u8; 32],
199 message_hash: &[u8; 32],
200 ) -> Result<[u8; 32]> {
201 let mut combined = Vec::with_capacity(66);
203 combined.extend_from_slice(b"\x19\x01");
204 combined.extend_from_slice(domain_hash);
205 combined.extend_from_slice(message_hash);
206 Ok(*keccak256(&combined))
207 }
208
209 fn sign_hash(&self, hash: &[u8; 32]) -> Result<String> {
210 let key_hex = self.private_key.as_hex();
212 let key_hex = key_hex.strip_prefix("0x").unwrap_or(key_hex);
213
214 let signer = PrivateKeySigner::from_str(key_hex)
216 .map_err(|e| Error::transport(format!("Failed to create signer: {e}")))?;
217
218 let hash_b256 = B256::from(*hash);
220
221 let signature = signer
223 .sign_hash_sync(&hash_b256)
224 .map_err(|e| Error::transport(format!("Failed to sign hash: {e}")))?;
225
226 let r = signature.r();
229 let s = signature.s();
230 let v = signature.v(); let v_byte = if v { 28u8 } else { 27u8 };
234
235 Ok(format!("0x{r:064x}{s:064x}{v_byte:02x}"))
237 }
238
239 fn canonicalize_action(action: &Value) -> Result<Value> {
240 match action {
241 Value::Object(obj) => {
242 let mut canonicalized = serde_json::Map::new();
243 for (key, value) in obj {
244 let canon_value = match key.as_str() {
245 "destination" | "address" | "user" if value.is_string() => {
246 Value::String(Self::canonicalize_address(value.as_str().unwrap()))
247 }
248 "amount" | "px" | "sz" | "price" | "size" if value.is_string() => {
249 Value::String(Self::canonicalize_decimal(value.as_str().unwrap()))
250 }
251 _ => Self::canonicalize_action(value)?,
252 };
253 canonicalized.insert(key.clone(), canon_value);
254 }
255 Ok(Value::Object(canonicalized))
256 }
257 Value::Array(arr) => {
258 let canonicalized: Result<Vec<_>> =
259 arr.iter().map(Self::canonicalize_action).collect();
260 Ok(Value::Array(canonicalized?))
261 }
262 _ => Ok(action.clone()),
263 }
264 }
265
266 fn canonicalize_address(addr: &str) -> String {
267 if addr.starts_with("0x") || addr.starts_with("0X") {
268 format!("0x{}", &addr[2..].to_lowercase())
269 } else {
270 format!("0x{}", addr.to_lowercase())
271 }
272 }
273
274 fn canonicalize_decimal(decimal: &str) -> String {
275 if let Ok(num) = decimal.parse::<f64>() {
276 if num.fract() == 0.0 {
277 format!("{num:.0}")
278 } else {
279 let trimmed = format!("{num}")
280 .trim_end_matches('0')
281 .trim_end_matches('.')
282 .to_string();
283 if trimmed.is_empty() || trimmed == "-" {
284 "0".to_string()
285 } else {
286 trimmed
287 }
288 }
289 } else {
290 decimal.to_string()
291 }
292 }
293
294 pub fn address(&self) -> Result<String> {
295 let key_hex = self.private_key.as_hex();
297 let key_hex = key_hex.strip_prefix("0x").unwrap_or(key_hex);
298
299 let signer = PrivateKeySigner::from_str(key_hex)
301 .map_err(|e| Error::transport(format!("Failed to create signer: {e}")))?;
302
303 let address = format!("{:#x}", signer.address());
305 Ok(address)
306 }
307}
308
309#[cfg(test)]
310mod tests {
311 use alloy_sol_types::SolStruct;
312 use rstest::rstest;
313 use serde_json::json;
314
315 use super::*;
316
317 #[rstest]
318 fn test_address_canonicalization() {
319 assert_eq!(
320 HyperliquidEip712Signer::canonicalize_address("0xABCDEF123456789"),
321 "0xabcdef123456789"
322 );
323 assert_eq!(
324 HyperliquidEip712Signer::canonicalize_address("ABCDEF123456789"),
325 "0xabcdef123456789"
326 );
327 assert_eq!(
328 HyperliquidEip712Signer::canonicalize_address("0XABCDEF123456789"),
329 "0xabcdef123456789"
330 );
331 }
332
333 #[rstest]
334 fn test_decimal_canonicalization() {
335 assert_eq!(
336 HyperliquidEip712Signer::canonicalize_decimal("100.000"),
337 "100"
338 );
339 assert_eq!(
340 HyperliquidEip712Signer::canonicalize_decimal("100.100"),
341 "100.1"
342 );
343 assert_eq!(HyperliquidEip712Signer::canonicalize_decimal("0.000"), "0");
344 assert_eq!(
345 HyperliquidEip712Signer::canonicalize_decimal("123.456"),
346 "123.456"
347 );
348 assert_eq!(
349 HyperliquidEip712Signer::canonicalize_decimal("123.450"),
350 "123.45"
351 );
352 }
353
354 #[rstest]
355 fn test_action_canonicalization() {
356 let action = json!({
357 "destination": "0xABCDEF123456789",
358 "amount": "100.000",
359 "other": "unchanged"
360 });
361
362 let canonicalized = HyperliquidEip712Signer::canonicalize_action(&action).unwrap();
363
364 assert_eq!(canonicalized["destination"], "0xabcdef123456789");
365 assert_eq!(canonicalized["amount"], "100");
366 assert_eq!(canonicalized["other"], "unchanged");
367 }
368
369 #[rstest]
370 fn test_sign_request_l1_action() {
371 let private_key = EvmPrivateKey::new(
372 "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".to_string(),
373 )
374 .unwrap();
375 let signer = HyperliquidEip712Signer::new(private_key);
376
377 let request = SignRequest {
378 action: json!({
379 "type": "withdraw",
380 "destination": "0xABCDEF123456789",
381 "amount": "100.000"
382 }),
383 action_bytes: None,
384 time_nonce: TimeNonce::from_millis(1640995200000),
385 action_type: HyperliquidActionType::L1,
386 is_testnet: false,
387 vault_address: None,
388 };
389
390 let result = signer.sign(&request).unwrap();
391 assert!(result.signature.starts_with("0x"));
393 assert_eq!(result.signature.len(), 132); }
395
396 #[rstest]
397 fn test_sign_request_user_action() {
398 let private_key = EvmPrivateKey::new(
399 "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".to_string(),
400 )
401 .unwrap();
402 let signer = HyperliquidEip712Signer::new(private_key);
403
404 let request = SignRequest {
405 action: json!({
406 "type": "order",
407 "coin": "BTC",
408 "px": "50000.00",
409 "sz": "0.1"
410 }),
411 action_bytes: None,
412 time_nonce: TimeNonce::from_millis(1640995200000),
413 action_type: HyperliquidActionType::UserSigned,
414 is_testnet: false,
415 vault_address: None,
416 };
417
418 let result = signer.sign(&request).unwrap();
419 assert!(result.signature.starts_with("0x"));
421 assert_eq!(result.signature.len(), 132); }
423
424 #[rstest]
425 fn test_connection_id_matches_python() {
426 use rust_decimal_macros::dec;
427
428 use crate::http::models::{
429 HyperliquidExecAction, HyperliquidExecGrouping, HyperliquidExecLimitParams,
430 HyperliquidExecOrderKind, HyperliquidExecPlaceOrderRequest, HyperliquidExecTif,
431 };
432
433 let private_key = EvmPrivateKey::new(
439 "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".to_string(),
440 )
441 .unwrap();
442 let signer = HyperliquidEip712Signer::new(private_key);
443
444 let typed_action = HyperliquidExecAction::Order {
453 orders: vec![HyperliquidExecPlaceOrderRequest {
454 asset: 0,
455 is_buy: true,
456 price: dec!(50000),
457 size: dec!(0.1),
458 reduce_only: false,
459 kind: HyperliquidExecOrderKind::Limit {
460 limit: HyperliquidExecLimitParams {
461 tif: HyperliquidExecTif::Gtc,
462 },
463 },
464 cloid: None,
465 }],
466 grouping: HyperliquidExecGrouping::Na,
467 builder: None,
468 };
469
470 let action_bytes = rmp_serde::to_vec_named(&typed_action).unwrap();
472 println!(
473 "Rust typed MsgPack bytes ({}): {}",
474 action_bytes.len(),
475 hex::encode(&action_bytes)
476 );
477
478 let python_msgpack = hex::decode(
480 "83a474797065a56f72646572a66f72646572739186a16100a162c3a170a53530303030a173a3302e31a172c2a17481a56c696d697481a3746966a3477463a867726f7570696e67a26e61",
481 )
482 .unwrap();
483 println!(
484 "Python MsgPack bytes ({}): {}",
485 python_msgpack.len(),
486 hex::encode(&python_msgpack)
487 );
488
489 assert_eq!(
491 hex::encode(&action_bytes),
492 hex::encode(&python_msgpack),
493 "MsgPack bytes should match Python"
494 );
495
496 let action_value = serde_json::to_value(&typed_action).unwrap();
498 let request = SignRequest {
499 action: action_value,
500 action_bytes: Some(action_bytes),
501 time_nonce: TimeNonce::from_millis(1640995200000),
502 action_type: HyperliquidActionType::L1,
503 is_testnet: true, vault_address: None,
505 };
506
507 let connection_id = signer.compute_connection_id(&request).unwrap();
508 println!(
509 "Rust Connection ID: {}",
510 hex::encode(connection_id.as_slice())
511 );
512
513 let expected_connection_id =
515 "207b9fb52defb524f5a7f1c80f069ff8b58556b018532401de0e1342bcb13b40";
516 assert_eq!(
517 hex::encode(connection_id.as_slice()),
518 expected_connection_id,
519 "Connection ID should match Python"
520 );
521
522 let source = "b".to_string(); let agent = Agent {
531 source,
532 connectionId: connection_id,
533 };
534
535 let domain = eip712_domain! {
536 name: "Exchange",
537 version: "1",
538 chain_id: 1337,
539 verifying_contract: Address::ZERO,
540 };
541
542 let signing_hash = agent.eip712_signing_hash(&domain);
543 println!(
544 "Rust EIP-712 signing hash: {}",
545 hex::encode(signing_hash.as_slice())
546 );
547
548 let expected_signing_hash =
550 "5242f54e0c01d3e7ef449f91b25c1a27802fdd221f7f24bc211da6bf7b847d8d";
551 assert_eq!(
552 hex::encode(signing_hash.as_slice()),
553 expected_signing_hash,
554 "EIP-712 signing hash should match Python"
555 );
556 }
557
558 #[rstest]
559 fn test_connection_id_matches_python_with_builder_fee() {
560 use rust_decimal_macros::dec;
561
562 use crate::http::models::{
563 HyperliquidExecAction, HyperliquidExecBuilderFee, HyperliquidExecGrouping,
564 HyperliquidExecLimitParams, HyperliquidExecOrderKind, HyperliquidExecPlaceOrderRequest,
565 HyperliquidExecTif,
566 };
567
568 let private_key = EvmPrivateKey::new(
575 "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".to_string(),
576 )
577 .unwrap();
578 let signer = HyperliquidEip712Signer::new(private_key);
579
580 let typed_action = HyperliquidExecAction::Order {
581 orders: vec![HyperliquidExecPlaceOrderRequest {
582 asset: 0,
583 is_buy: true,
584 price: dec!(50000),
585 size: dec!(0.1),
586 reduce_only: false,
587 kind: HyperliquidExecOrderKind::Limit {
588 limit: HyperliquidExecLimitParams {
589 tif: HyperliquidExecTif::Gtc,
590 },
591 },
592 cloid: None,
593 }],
594 grouping: HyperliquidExecGrouping::Na,
595 builder: Some(HyperliquidExecBuilderFee {
596 address: "0x9b6e2fe4124ed3e7a6b4f85e7860e032222b4316".to_string(),
597 fee_tenths_bp: 1,
598 }),
599 };
600
601 let action_bytes = rmp_serde::to_vec_named(&typed_action).unwrap();
603 println!(
604 "Rust typed MsgPack bytes with builder ({}): {}",
605 action_bytes.len(),
606 hex::encode(&action_bytes)
607 );
608
609 let python_msgpack = hex::decode(
611 "84a474797065a56f72646572a66f72646572739186a16100a162c3a170a53530303030a173a3302e31a172c2a17481a56c696d697481a3746966a3477463a867726f7570696e67a26e61a76275696c64657282a162d92a307839623665326665343132346564336537613662346638356537383630653033323232326234333136a16601",
612 )
613 .unwrap();
614 println!(
615 "Python MsgPack bytes with builder ({}): {}",
616 python_msgpack.len(),
617 hex::encode(&python_msgpack)
618 );
619
620 assert_eq!(
622 hex::encode(&action_bytes),
623 hex::encode(&python_msgpack),
624 "MsgPack bytes with builder should match Python"
625 );
626
627 let action_value = serde_json::to_value(&typed_action).unwrap();
629 let request = SignRequest {
630 action: action_value,
631 action_bytes: Some(action_bytes),
632 time_nonce: TimeNonce::from_millis(1640995200000),
633 action_type: HyperliquidActionType::L1,
634 is_testnet: true,
635 vault_address: None,
636 };
637
638 let connection_id = signer.compute_connection_id(&request).unwrap();
639 println!(
640 "Rust Connection ID with builder: {}",
641 hex::encode(connection_id.as_slice())
642 );
643
644 let expected_connection_id =
645 "235d93388ffa044d5fb14a7fe8103a8a29b73d1e2049cd086e7903671a6cfb49";
646 assert_eq!(
647 hex::encode(connection_id.as_slice()),
648 expected_connection_id,
649 "Connection ID with builder should match Python"
650 );
651
652 let source = "b".to_string();
654 let agent = Agent {
655 source,
656 connectionId: connection_id,
657 };
658
659 let domain = eip712_domain! {
660 name: "Exchange",
661 version: "1",
662 chain_id: 1337,
663 verifying_contract: Address::ZERO,
664 };
665
666 let signing_hash = agent.eip712_signing_hash(&domain);
667 println!(
668 "Rust EIP-712 signing hash with builder: {}",
669 hex::encode(signing_hash.as_slice())
670 );
671
672 let expected_signing_hash =
673 "6f046f4b02e79610b8cf26c73505f8de3ff1d91d6953c5e972fbf198a5311a41";
674 assert_eq!(
675 hex::encode(signing_hash.as_slice()),
676 expected_signing_hash,
677 "EIP-712 signing hash with builder should match Python"
678 );
679 }
680
681 #[rstest]
682 fn test_connection_id_with_cloid() {
683 use rust_decimal_macros::dec;
684
685 use crate::http::models::{
686 Cloid, HyperliquidExecAction, HyperliquidExecBuilderFee, HyperliquidExecGrouping,
687 HyperliquidExecLimitParams, HyperliquidExecOrderKind, HyperliquidExecPlaceOrderRequest,
688 HyperliquidExecTif,
689 };
690
691 let private_key = EvmPrivateKey::new(
695 "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".to_string(),
696 )
697 .unwrap();
698 let _signer = HyperliquidEip712Signer::new(private_key);
699
700 let cloid = Cloid::from_hex("0x1234567890abcdef1234567890abcdef").unwrap();
702 println!("Cloid hex: {}", cloid.to_hex());
703
704 let typed_action = HyperliquidExecAction::Order {
705 orders: vec![HyperliquidExecPlaceOrderRequest {
706 asset: 0,
707 is_buy: true,
708 price: dec!(50000),
709 size: dec!(0.1),
710 reduce_only: false,
711 kind: HyperliquidExecOrderKind::Limit {
712 limit: HyperliquidExecLimitParams {
713 tif: HyperliquidExecTif::Gtc,
714 },
715 },
716 cloid: Some(cloid),
717 }],
718 grouping: HyperliquidExecGrouping::Na,
719 builder: Some(HyperliquidExecBuilderFee {
720 address: "0x9b6e2fe4124ed3e7a6b4f85e7860e032222b4316".to_string(),
721 fee_tenths_bp: 1,
722 }),
723 };
724
725 let action_bytes = rmp_serde::to_vec_named(&typed_action).unwrap();
727 println!(
728 "Rust MsgPack bytes with cloid ({}): {}",
729 action_bytes.len(),
730 hex::encode(&action_bytes)
731 );
732
733 let decoded: serde_json::Value = rmp_serde::from_slice(&action_bytes).unwrap();
735 println!(
736 "Decoded structure: {}",
737 serde_json::to_string_pretty(&decoded).unwrap()
738 );
739
740 let orders = decoded.get("orders").unwrap().as_array().unwrap();
742 let first_order = &orders[0];
743 let cloid_field = first_order.get("c").unwrap();
744 println!("Cloid in msgpack: {cloid_field}");
745 assert_eq!(
746 cloid_field.as_str().unwrap(),
747 "0x1234567890abcdef1234567890abcdef"
748 );
749
750 let order_json = serde_json::to_string(first_order).unwrap();
752 println!("Order JSON: {order_json}");
753 }
754
755 #[rstest]
756 fn test_cloid_from_client_order_id() {
757 use nautilus_model::identifiers::ClientOrderId;
758
759 use crate::http::models::Cloid;
760
761 let client_order_id = ClientOrderId::from("O-20241210-123456-001-001-1");
764 let cloid = Cloid::from_client_order_id(client_order_id);
765
766 println!("ClientOrderId: {client_order_id}");
767 println!("Cloid hex: {}", cloid.to_hex());
768
769 let hex = cloid.to_hex();
771 assert!(hex.starts_with("0x"), "Should start with 0x");
772 assert_eq!(hex.len(), 34, "Should be 34 chars (0x + 32 hex)");
773
774 for c in hex[2..].chars() {
776 assert!(c.is_ascii_hexdigit(), "Should be hex digit: {c}");
777 }
778
779 let json = serde_json::to_string(&cloid).unwrap();
781 println!("Cloid JSON: {json}");
782 assert!(json.contains(&hex));
783 }
784
785 #[rstest]
786 fn test_production_like_order_with_hashed_cloid() {
787 use nautilus_model::identifiers::ClientOrderId;
788 use rust_decimal_macros::dec;
789
790 use crate::http::models::{
791 Cloid, HyperliquidExecAction, HyperliquidExecBuilderFee, HyperliquidExecGrouping,
792 HyperliquidExecLimitParams, HyperliquidExecOrderKind, HyperliquidExecPlaceOrderRequest,
793 HyperliquidExecTif,
794 };
795
796 let private_key = EvmPrivateKey::new(
799 "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".to_string(),
800 )
801 .unwrap();
802 let signer = HyperliquidEip712Signer::new(private_key);
803
804 let client_order_id = ClientOrderId::from("O-20241210-123456-001-001-1");
806 let cloid = Cloid::from_client_order_id(client_order_id);
807
808 println!("=== Production-like Order ===");
809 println!("ClientOrderId: {client_order_id}");
810 println!("Cloid: {}", cloid.to_hex());
811
812 let typed_action = HyperliquidExecAction::Order {
813 orders: vec![HyperliquidExecPlaceOrderRequest {
814 asset: 3, is_buy: true,
816 price: dec!(92572.0),
817 size: dec!(0.001),
818 reduce_only: false,
819 kind: HyperliquidExecOrderKind::Limit {
820 limit: HyperliquidExecLimitParams {
821 tif: HyperliquidExecTif::Gtc,
822 },
823 },
824 cloid: Some(cloid),
825 }],
826 grouping: HyperliquidExecGrouping::Na,
827 builder: Some(HyperliquidExecBuilderFee {
828 address: "0x9b6e2fe4124ed3e7a6b4f85e7860e032222b4316".to_string(),
829 fee_tenths_bp: 1,
830 }),
831 };
832
833 let action_bytes = rmp_serde::to_vec_named(&typed_action).unwrap();
835 println!(
836 "MsgPack bytes ({}): {}",
837 action_bytes.len(),
838 hex::encode(&action_bytes)
839 );
840
841 let decoded: serde_json::Value = rmp_serde::from_slice(&action_bytes).unwrap();
843 println!(
844 "Decoded: {}",
845 serde_json::to_string_pretty(&decoded).unwrap()
846 );
847
848 let action_value = serde_json::to_value(&typed_action).unwrap();
850 let request = SignRequest {
851 action: action_value,
852 action_bytes: Some(action_bytes),
853 time_nonce: TimeNonce::from_millis(1733833200000), action_type: HyperliquidActionType::L1,
855 is_testnet: true, vault_address: None,
857 };
858
859 let connection_id = signer.compute_connection_id(&request).unwrap();
860 println!("Connection ID: {}", hex::encode(connection_id.as_slice()));
861
862 let source = "b".to_string();
864 let agent = Agent {
865 source,
866 connectionId: connection_id,
867 };
868
869 let domain = eip712_domain! {
870 name: "Exchange",
871 version: "1",
872 chain_id: 1337,
873 verifying_contract: Address::ZERO,
874 };
875
876 let signing_hash = agent.eip712_signing_hash(&domain);
877 println!("Signing hash: {}", hex::encode(signing_hash.as_slice()));
878
879 let result = signer.sign(&request).unwrap();
881 println!("Signature: {}", result.signature);
882 assert!(result.signature.starts_with("0x"));
883 assert_eq!(result.signature.len(), 132);
884 }
885
886 #[rstest]
887 fn test_price_decimal_formatting() {
888 use nautilus_model::types::Price;
889 use rust_decimal_macros::dec;
890
891 let test_cases = [
894 (92572.0_f64, 1_u8, "92572"), (92572.5, 1, "92572.5"), (0.001, 8, "0.001"), (50000.0, 1, "50000"), (0.1, 4, "0.1"), ];
900
901 for (value, precision, expected_normalized) in test_cases {
902 let price = Price::new(value, precision);
903 let price_decimal = price.as_decimal();
904 let normalized = price_decimal.normalize();
905
906 println!(
907 "Price({value}, {precision}) -> as_decimal: {price_decimal:?} -> normalized: {normalized}"
908 );
909
910 assert_eq!(
911 normalized.to_string(),
912 expected_normalized,
913 "Price({value}, {precision}) should normalize to {expected_normalized}"
914 );
915 }
916
917 let price_from_type = Price::new(92572.0, 1).as_decimal().normalize();
919 let price_from_dec = dec!(92572.0).normalize();
920 assert_eq!(
921 price_from_type.to_string(),
922 price_from_dec.to_string(),
923 "Price::as_decimal should match dec! macro"
924 );
925 }
926}