1use crate::adapter::SingleAddressSigner;
9use crate::signer::Signer;
10use crate::types::{ExchangeError, Signature};
11
12use motosan_wallet_core::HlTypeField;
13
14#[derive(Debug, Clone)]
23pub struct EIP712Field {
24 pub name: String,
25 pub field_type: String,
26}
27
28impl EIP712Field {
29 pub fn new(name: &str, field_type: &str) -> Self {
30 Self {
31 name: name.to_string(),
32 field_type: field_type.to_string(),
33 }
34 }
35}
36
37pub fn compute_action_hash(
51 action: &serde_json::Value,
52 vault_address: Option<&str>,
53 nonce: u64,
54) -> Result<[u8; 32], ExchangeError> {
55 motosan_wallet_core::compute_action_hash(action, nonce, vault_address)
56 .map_err(|e| ExchangeError::SerializationError(e.to_string()))
57}
58
59pub fn compute_action_hash_with_expiry(
64 action: &serde_json::Value,
65 vault_address: Option<&str>,
66 nonce: u64,
67 expires_after: Option<u64>,
68) -> Result<[u8; 32], ExchangeError> {
69 match expires_after {
70 None => compute_action_hash(action, vault_address, nonce),
71 Some(expires) => {
72 use alloy_primitives::keccak256;
75
76 let action_bytes = rmp_serde::to_vec_named(action)
77 .map_err(|e| ExchangeError::SerializationError(e.to_string()))?;
78
79 let mut data = Vec::with_capacity(action_bytes.len() + 8 + 21 + 8);
80 data.extend_from_slice(&action_bytes);
81 data.extend_from_slice(&nonce.to_be_bytes());
82
83 match vault_address {
84 None => data.push(0x00),
85 Some(addr) => {
86 data.push(0x01);
87 let addr_bytes = parse_address_bytes(addr)?;
88 data.extend_from_slice(&addr_bytes);
89 }
90 }
91
92 data.extend_from_slice(&expires.to_be_bytes());
93 Ok(keccak256(&data).into())
94 }
95 }
96}
97
98pub fn sign_l1_action(
108 signer: &dyn Signer,
109 address: &str,
110 action: &serde_json::Value,
111 nonce: u64,
112 is_mainnet: bool,
113 vault_address: Option<&str>,
114) -> Result<Signature, ExchangeError> {
115 let adapter = SingleAddressSigner::new(signer, address.to_string());
116 let hl_sig =
117 motosan_wallet_core::sign_l1_action(&adapter, action, nonce, is_mainnet, vault_address)
118 .map_err(|e| ExchangeError::SigningError(e.to_string()))?;
119 Ok(hl_signature_to_signature(&hl_sig))
120}
121
122pub fn sign_user_signed_action(
133 signer: &dyn Signer,
134 address: &str,
135 action: &serde_json::Value,
136 types: &[EIP712Field],
137 primary_type: &str,
138 is_mainnet: bool,
139) -> Result<Signature, ExchangeError> {
140 let hl_fields: Vec<HlTypeField<'_>> = types
142 .iter()
143 .map(|f| HlTypeField::new(&f.name, &f.field_type))
144 .collect();
145
146 let adapter = SingleAddressSigner::new(signer, address.to_string());
147 let hl_sig = motosan_wallet_core::sign_user_signed_action(
148 &adapter,
149 action,
150 &hl_fields,
151 primary_type,
152 is_mainnet,
153 )
154 .map_err(|e| ExchangeError::SigningError(e.to_string()))?;
155
156 Ok(hl_signature_to_signature(&hl_sig))
157}
158
159fn hl_signature_to_signature(hl: &motosan_wallet_core::HlSignature) -> Signature {
165 Signature {
166 r: hl.r.clone(),
167 s: hl.s.clone(),
168 v: hl.v,
169 }
170}
171
172fn parse_address_bytes(address: &str) -> Result<[u8; 20], ExchangeError> {
175 let stripped = address
176 .strip_prefix("0x")
177 .or_else(|| address.strip_prefix("0X"))
178 .unwrap_or(address);
179 let bytes = hex::decode(stripped)
180 .map_err(|e| ExchangeError::InvalidAddress(format!("Invalid hex: {}", e)))?;
181 if bytes.len() != 20 {
182 return Err(ExchangeError::InvalidAddress(format!(
183 "Expected 20 bytes, got {}",
184 bytes.len()
185 )));
186 }
187 let mut result = [0u8; 20];
188 result.copy_from_slice(&bytes);
189 Ok(result)
190}
191
192#[cfg(test)]
197mod tests {
198 use super::*;
199
200 struct TestSigner {
202 key: k256::ecdsa::SigningKey,
203 address: String,
204 }
205
206 impl TestSigner {
207 fn new(hex_key: &str) -> Self {
208 let stripped = hex_key.strip_prefix("0x").unwrap_or(hex_key);
209 let key_bytes = hex::decode(stripped).unwrap();
210 let key = k256::ecdsa::SigningKey::from_bytes((&key_bytes[..]).into()).unwrap();
211
212 let verifying_key = key.verifying_key();
214 let point = verifying_key.to_encoded_point(false);
215 let pubkey_bytes = &point.as_bytes()[1..];
216 use sha3::{Digest, Keccak256};
217 let hash = Keccak256::digest(pubkey_bytes);
218 let address = format!("0x{}", hex::encode(&hash[12..]));
219
220 Self { key, address }
221 }
222
223 fn address(&self) -> &str {
224 &self.address
225 }
226 }
227
228 impl Signer for TestSigner {
229 fn sign_hash(&self, _address: &str, hash: &[u8; 32]) -> Result<[u8; 65], ExchangeError> {
230 use k256::ecdsa::{signature::hazmat::PrehashSigner, RecoveryId};
231 let (signature, recovery_id): (k256::ecdsa::Signature, RecoveryId) = self
232 .key
233 .sign_prehash(hash)
234 .map_err(|e| ExchangeError::SigningError(format!("k256 signing error: {}", e)))?;
235 let mut result = [0u8; 65];
236 result[..64].copy_from_slice(&signature.to_bytes());
237 result[64] = recovery_id.to_byte();
238 Ok(result)
239 }
240 }
241
242 const TEST_KEY: &str = "0x4c0883a69102937d6231471b5dbb6204fe512961708279f22a82e1e0e3e1d0a2";
243
244 #[test]
245 fn test_compute_action_hash_deterministic() {
246 let action = serde_json::json!({
247 "type": "order",
248 "orders": [{"a": 0, "b": true, "p": "30000", "s": "0.1"}],
249 "grouping": "na"
250 });
251 let hash1 = compute_action_hash(&action, None, 1234567890).unwrap();
252 let hash2 = compute_action_hash(&action, None, 1234567890).unwrap();
253 assert_eq!(hash1, hash2);
254 }
255
256 #[test]
257 fn test_compute_action_hash_different_nonce() {
258 let action = serde_json::json!({"type": "order"});
259 let hash1 = compute_action_hash(&action, None, 100).unwrap();
260 let hash2 = compute_action_hash(&action, None, 200).unwrap();
261 assert_ne!(hash1, hash2);
262 }
263
264 #[test]
265 fn test_compute_action_hash_with_vault_address() {
266 let action = serde_json::json!({"type": "order"});
267 let hash_no_vault = compute_action_hash(&action, None, 100).unwrap();
268 let hash_with_vault = compute_action_hash(
269 &action,
270 Some("0x1234567890abcdef1234567890abcdef12345678"),
271 100,
272 )
273 .unwrap();
274 assert_ne!(hash_no_vault, hash_with_vault);
275 }
276
277 #[test]
278 fn test_sign_l1_action_produces_valid_signature() {
279 let signer = TestSigner::new(TEST_KEY);
280 let addr = signer.address().to_string();
281 let action = serde_json::json!({
282 "type": "order",
283 "orders": [{"a": 0, "b": true, "p": "30000", "s": "0.1"}],
284 "grouping": "na"
285 });
286
287 let sig = sign_l1_action(&signer, &addr, &action, 1234567890, true, None).unwrap();
288
289 assert!(sig.r.starts_with("0x"));
291 assert!(sig.s.starts_with("0x"));
292 assert_eq!(sig.r.len(), 66); assert_eq!(sig.s.len(), 66);
294 assert!(sig.v == 27 || sig.v == 28);
295 }
296
297 #[test]
298 fn test_sign_l1_action_mainnet_vs_testnet() {
299 let signer = TestSigner::new(TEST_KEY);
300 let addr = signer.address().to_string();
301 let action = serde_json::json!({"type": "order"});
302
303 let sig_mainnet = sign_l1_action(&signer, &addr, &action, 100, true, None).unwrap();
304 let sig_testnet = sign_l1_action(&signer, &addr, &action, 100, false, None).unwrap();
305
306 assert_ne!(sig_mainnet.r, sig_testnet.r);
308 }
309
310 #[test]
311 fn test_sign_l1_action_recoverable() {
312 let signer = TestSigner::new(TEST_KEY);
313 let addr = signer.address().to_string();
314 let action = serde_json::json!({"type": "order"});
315
316 let sig = sign_l1_action(&signer, &addr, &action, 100, true, None).unwrap();
317
318 let action_hash = compute_action_hash(&action, None, 100).unwrap();
321
322 use alloy_primitives::keccak256;
324 let connection_id_hex = format!("0x{}", hex::encode(action_hash));
325 let _agent_action = serde_json::json!({
326 "source": "a",
327 "connectionId": connection_id_hex,
328 });
329
330 let type_string = "Agent(string source,bytes32 connectionId)";
332 let type_hash: [u8; 32] = keccak256(type_string.as_bytes()).into();
333
334 let domain_type_hash = keccak256(
336 b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)",
337 );
338 let name_hash = keccak256(b"Exchange");
339 let version_hash = keccak256(b"1");
340 let mut chain_id_bytes = [0u8; 32];
341 chain_id_bytes[24..32].copy_from_slice(&1337u64.to_be_bytes());
342 let contract_bytes = [0u8; 32];
343
344 let mut domain_buf = Vec::with_capacity(32 * 5);
345 domain_buf.extend_from_slice(domain_type_hash.as_slice());
346 domain_buf.extend_from_slice(name_hash.as_slice());
347 domain_buf.extend_from_slice(version_hash.as_slice());
348 domain_buf.extend_from_slice(&chain_id_bytes);
349 domain_buf.extend_from_slice(&contract_bytes);
350 let domain_sep: [u8; 32] = keccak256(&domain_buf).into();
351
352 let source_hash: [u8; 32] = keccak256(b"a").into();
354 let conn_bytes = hex::decode(connection_id_hex.strip_prefix("0x").unwrap()).unwrap();
355 let mut struct_buf = Vec::with_capacity(32 * 3);
356 struct_buf.extend_from_slice(&type_hash);
357 struct_buf.extend_from_slice(&source_hash);
358 struct_buf.extend_from_slice(&conn_bytes);
359 let struct_hash: [u8; 32] = keccak256(&struct_buf).into();
360
361 let mut eip712_data = Vec::with_capacity(66);
362 eip712_data.extend_from_slice(&[0x19, 0x01]);
363 eip712_data.extend_from_slice(&domain_sep);
364 eip712_data.extend_from_slice(&struct_hash);
365 let final_hash: [u8; 32] = keccak256(&eip712_data).into();
366
367 let r_bytes = hex::decode(sig.r.strip_prefix("0x").unwrap()).unwrap();
369 let s_bytes = hex::decode(sig.s.strip_prefix("0x").unwrap()).unwrap();
370 let v = sig.v - 27;
371
372 let mut sig_bytes = [0u8; 64];
373 sig_bytes[..32].copy_from_slice(&r_bytes);
374 sig_bytes[32..].copy_from_slice(&s_bytes);
375
376 use k256::ecdsa::{RecoveryId, Signature as K256Sig, VerifyingKey};
377 let signature = K256Sig::from_slice(&sig_bytes).unwrap();
378 let recovery_id = RecoveryId::from_byte(v).unwrap();
379 let recovered =
380 VerifyingKey::recover_from_prehash(&final_hash, &signature, recovery_id).unwrap();
381
382 let point = recovered.to_encoded_point(false);
384 let pubkey_bytes = &point.as_bytes()[1..];
385 use sha3::{Digest, Keccak256};
386 let hash = Keccak256::digest(pubkey_bytes);
387 let recovered_addr = format!("0x{}", hex::encode(&hash[12..]));
388
389 assert_eq!(recovered_addr, addr);
390 }
391
392 #[test]
393 fn test_sign_user_signed_action() {
394 let signer = TestSigner::new(TEST_KEY);
395 let addr = signer.address().to_string();
396
397 let action = serde_json::json!({
398 "type": "approveAgent",
399 "agentAddress": "0x1234567890abcdef1234567890abcdef12345678",
400 "agentName": "test-agent",
401 "nonce": 1000
402 });
403
404 let types = vec![
405 EIP712Field::new("hyperliquidChain", "string"),
406 EIP712Field::new("agentAddress", "address"),
407 EIP712Field::new("agentName", "string"),
408 EIP712Field::new("nonce", "uint64"),
409 ];
410
411 let sig = sign_user_signed_action(
412 &signer,
413 &addr,
414 &action,
415 &types,
416 "HyperliquidTransaction:ApproveAgent",
417 true,
418 )
419 .unwrap();
420
421 assert!(sig.r.starts_with("0x"));
422 assert!(sig.s.starts_with("0x"));
423 assert_eq!(sig.r.len(), 66);
424 assert_eq!(sig.s.len(), 66);
425 assert!(sig.v == 27 || sig.v == 28);
426 }
427
428 #[test]
429 fn test_parse_address_bytes_valid() {
430 let addr = "0x1234567890abcdef1234567890abcdef12345678";
431 let bytes = parse_address_bytes(addr).unwrap();
432 assert_eq!(bytes.len(), 20);
433 assert_eq!(bytes[0], 0x12);
434 assert_eq!(bytes[19], 0x78);
435 }
436
437 #[test]
438 fn test_parse_address_bytes_invalid() {
439 let result = parse_address_bytes("0xinvalid");
440 assert!(result.is_err());
441 }
442
443 #[test]
444 fn test_exchange_client_url_construction() {
445 let mainnet_url = crate::client::ExchangeClient::base_url_for(true);
446 let testnet_url = crate::client::ExchangeClient::base_url_for(false);
447 assert_eq!(mainnet_url, "https://api.hyperliquid.xyz");
448 assert_eq!(testnet_url, "https://api.hyperliquid-testnet.xyz");
449 }
450}