quicknode_hyperliquid_sdk/
signing.rs1use alloy::primitives::{keccak256, Address, B256};
6use alloy::signers::local::PrivateKeySigner;
7use alloy::signers::Signer;
8use alloy::sol;
9use alloy::sol_types::SolStruct;
10use serde::Serialize;
11
12use crate::types::{Chain, Signature, CORE_MAINNET_EIP712_DOMAIN};
13
14sol! {
16 struct Agent {
17 string source;
18 bytes32 connectionId;
19 }
20}
21
22#[inline]
26pub fn agent_signing_hash(chain: Chain, connection_id: B256) -> B256 {
27 let agent = Agent {
28 source: if chain.is_mainnet() { "a" } else { "b" }.to_string(),
29 connectionId: connection_id,
30 };
31 agent.eip712_signing_hash(&CORE_MAINNET_EIP712_DOMAIN)
32}
33
34pub fn rmp_hash<T: Serialize>(
40 value: &T,
41 nonce: u64,
42 vault_address: Option<Address>,
43 expires_after: Option<u64>,
44) -> Result<B256, rmp_serde::encode::Error> {
45 let mut bytes = rmp_serde::to_vec_named(value)?;
46 bytes.extend(nonce.to_be_bytes());
47
48 if let Some(vault_address) = vault_address {
49 bytes.push(1);
50 bytes.extend(vault_address.as_slice());
51 } else {
52 bytes.push(0);
53 }
54
55 if let Some(expires_after) = expires_after {
56 bytes.push(0);
57 bytes.extend(expires_after.to_be_bytes());
58 }
59
60 Ok(keccak256(bytes))
61}
62
63pub async fn sign_hash(signer: &PrivateKeySigner, hash: B256) -> crate::Result<Signature> {
65 let sig = signer
66 .sign_hash(&hash)
67 .await
68 .map_err(|e| crate::Error::SigningError(e.to_string()))?;
69 Ok(sig.into())
70}
71
72pub async fn sign_action<T: Serialize>(
74 signer: &PrivateKeySigner,
75 chain: Chain,
76 action: &T,
77 nonce: u64,
78 vault_address: Option<Address>,
79 expires_after: Option<u64>,
80) -> crate::Result<Signature> {
81 let connection_id = rmp_hash(action, nonce, vault_address, expires_after)
83 .map_err(|e| crate::Error::SigningError(format!("MessagePack serialization failed: {}", e)))?;
84
85 let signing_hash = agent_signing_hash(chain, connection_id);
87
88 sign_hash(signer, signing_hash).await
90}
91
92pub fn recover_signer(hash: B256, sig: &Signature) -> crate::Result<Address> {
94 let alloy_sig = alloy::signers::Signature::new(
95 alloy::primitives::U256::from(sig.r),
96 alloy::primitives::U256::from(sig.s),
97 sig.v == 28,
98 );
99
100 alloy_sig
101 .recover_address_from_prehash(&hash)
102 .map_err(|e| crate::Error::SigningError(format!("Failed to recover signer: {}", e)))
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use alloy::primitives::B256;
109
110 #[test]
111 fn test_agent_signing_hash_mainnet() {
112 let connection_id = B256::ZERO;
113 let hash = agent_signing_hash(Chain::Mainnet, connection_id);
114 assert!(!hash.is_zero());
116 }
117
118 #[test]
119 fn test_agent_signing_hash_testnet() {
120 let connection_id = B256::ZERO;
121 let hash_mainnet = agent_signing_hash(Chain::Mainnet, connection_id);
122 let hash_testnet = agent_signing_hash(Chain::Testnet, connection_id);
123 assert_ne!(hash_mainnet, hash_testnet);
125 }
126
127 #[test]
128 fn test_rmp_hash_deterministic() {
129 #[derive(Serialize)]
130 struct TestAction {
131 value: u64,
132 }
133
134 let action = TestAction { value: 42 };
135 let hash1 = rmp_hash(&action, 1000, None, None).unwrap();
136 let hash2 = rmp_hash(&action, 1000, None, None).unwrap();
137 assert_eq!(hash1, hash2);
138 }
139
140 #[test]
141 fn test_rmp_hash_with_vault() {
142 #[derive(Serialize)]
143 struct TestAction {
144 value: u64,
145 }
146
147 let action = TestAction { value: 42 };
148 let vault = Address::ZERO;
149 let hash_no_vault = rmp_hash(&action, 1000, None, None).unwrap();
150 let hash_with_vault = rmp_hash(&action, 1000, Some(vault), None).unwrap();
151 assert_ne!(hash_no_vault, hash_with_vault);
153 }
154}