1use k256::ecdsa::SigningKey;
28use sha3::{Digest, Keccak256};
29use std::sync::atomic::{AtomicI64, Ordering};
30use std::time::{Duration, SystemTime, UNIX_EPOCH};
31
32pub const CHAIN_ID: u64 = 8453;
36
37pub const DOMAIN_NAME: &str = "Limitless CTF Exchange";
39
40pub const DOMAIN_VERSION: &str = "1";
42
43pub const ORDER_TYPE_NAME: &str = "Order";
45
46pub const ORDER_TYPE: &str =
48 "Order(uint256 salt,address maker,address signer,address taker,uint256 tokenId,uint256 makerAmount,uint256 takerAmount,uint256 expiration,uint256 nonce,uint256 feeRateBps,uint8 side,uint8 signatureType)";
49
50static LAST_ORDER_SALT: AtomicI64 = AtomicI64::new(0);
56
57fn keccak256(data: &[u8]) -> [u8; 32] {
61 let mut hasher = Keccak256::new();
62 hasher.update(data);
63 let result = hasher.finalize();
64 let mut hash = [0u8; 32];
65 hash.copy_from_slice(&result);
66 hash
67}
68
69fn encode_u256_from_u64(value: u64) -> [u8; 32] {
73 let mut buf = [0u8; 32];
74 buf[24..].copy_from_slice(&value.to_be_bytes());
75 buf
76}
77
78fn encode_u256_from_i64(value: i64) -> Result<[u8; 32], String> {
81 if value < 0 {
82 return Err(format!("expected non-negative integer, got {value}"));
83 }
84 Ok(encode_u256_from_u64(value as u64))
85}
86
87fn encode_u256_from_i32(value: i32) -> Result<[u8; 32], String> {
89 if value < 0 {
90 return Err(format!("expected non-negative integer, got {value}"));
91 }
92 Ok(encode_u256_from_u64(value as u64))
93}
94
95fn encode_decimal_string_as_u256(value: &str) -> Result<[u8; 32], String> {
99 if value.is_empty() || !value.chars().all(|c| c.is_ascii_digit()) {
100 return Err(format!("invalid uint value: {value}"));
101 }
102 let trimmed = value.trim_start_matches('0');
104 let digits = if trimmed.is_empty() { "0" } else { trimmed };
105
106 let mut bytes: Vec<u8> = Vec::new();
108 let mut current = digits.to_string();
109
110 while !current.is_empty() && current != "0" {
111 let mut next = String::new();
112 let mut carry = 0u32;
113 for c in current.chars() {
114 let val = carry * 10 + (c as u32 - '0' as u32);
115 let q = val / 256;
116 carry = val % 256;
117 if !next.is_empty() || q > 0 {
118 next.push(char::from_digit(q, 10).unwrap_or('0'));
119 }
120 }
121 bytes.push(carry as u8);
122 if next.is_empty() {
123 next = String::from("0");
124 }
125 current = next;
126 }
127
128 bytes.reverse();
129 if bytes.len() > 32 {
130 return Err(format!("value {} exceeds uint256 size", value));
131 }
132
133 let mut out = [0u8; 32];
134 let start = 32 - bytes.len();
135 out[start..].copy_from_slice(&bytes);
136 Ok(out)
137}
138
139
140fn encode_address(addr: &[u8; 20]) -> [u8; 32] {
142 let mut buf = [0u8; 32];
143 buf[12..].copy_from_slice(addr);
144 buf
145}
146
147fn encode_string(s: &str) -> [u8; 32] {
149 keccak256(s.as_bytes())
150}
151
152pub fn generate_salt() -> i64 {
159 let now = SystemTime::now()
160 .duration_since(UNIX_EPOCH)
161 .unwrap_or_else(|_| Duration::from_millis(0));
162 let candidate = i64::try_from(now.as_micros()).unwrap_or(i64::MAX - 1);
163
164 loop {
165 let previous = LAST_ORDER_SALT.load(Ordering::Relaxed);
166 let next = candidate.max(previous.saturating_add(1));
167 match LAST_ORDER_SALT.compare_exchange(previous, next, Ordering::SeqCst, Ordering::SeqCst) {
168 Ok(_) => return next,
169 Err(_) => continue,
170 }
171 }
172}
173
174pub fn domain_separator(verifying_contract: &[u8; 20]) -> [u8; 32] {
180 let domain_type =
182 "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)";
183 let type_hash = keccak256(domain_type.as_bytes());
184
185 let name_hash = encode_string(DOMAIN_NAME);
186 let version_hash = encode_string(DOMAIN_VERSION);
187
188 let chain_id_bytes = encode_u256_from_u64(CHAIN_ID);
190
191 let contract = encode_address(verifying_contract);
192
193 let mut data = Vec::with_capacity(32 * 5);
195 data.extend_from_slice(&type_hash);
196 data.extend_from_slice(&name_hash);
197 data.extend_from_slice(&version_hash);
198 data.extend_from_slice(&chain_id_bytes);
199 data.extend_from_slice(&contract);
200
201 keccak256(&data)
202}
203
204pub fn order_type_hash() -> [u8; 32] {
206 keccak256(ORDER_TYPE.as_bytes())
207}
208
209pub fn hash_order(
213 salt: i64,
214 maker: &[u8; 20],
215 signer: &[u8; 20],
216 taker: &[u8; 20],
217 token_id: &str,
218 maker_amount: i64,
219 taker_amount: i64,
220 expiration: &str,
221 nonce: i32,
222 fee_rate_bps: i32,
223 side: u8,
224 signature_type: u8,
225) -> Result<[u8; 32], String> {
226 let type_hash = order_type_hash();
227
228 let mut data = Vec::with_capacity(32 * 13);
229 data.extend_from_slice(&type_hash);
230 data.extend_from_slice(&encode_u256_from_i64(salt)?);
231 data.extend_from_slice(&encode_address(maker));
232 data.extend_from_slice(&encode_address(signer));
233 data.extend_from_slice(&encode_address(taker));
234 data.extend_from_slice(&encode_decimal_string_as_u256(token_id)?);
235 data.extend_from_slice(&encode_u256_from_i64(maker_amount)?);
236 data.extend_from_slice(&encode_u256_from_i64(taker_amount)?);
237 data.extend_from_slice(&encode_decimal_string_as_u256(expiration)?);
238 data.extend_from_slice(&encode_u256_from_i32(nonce)?);
239 data.extend_from_slice(&encode_u256_from_i32(fee_rate_bps)?);
240 data.extend_from_slice(&encode_u256_from_u64(side as u64));
241 data.extend_from_slice(&encode_u256_from_u64(signature_type as u64));
242
243 Ok(keccak256(&data))
244}
245
246pub fn eip712_message_hash(domain_separator: &[u8; 32], order_hash: &[u8; 32]) -> [u8; 32] {
250 let mut data = Vec::with_capacity(2 + 32 + 32);
251 data.extend_from_slice(b"\x19\x01");
252 data.extend_from_slice(domain_separator);
253 data.extend_from_slice(order_hash);
254 keccak256(&data)
255}
256
257pub fn parse_address(addr: &str) -> Result<[u8; 20], String> {
261 let hex_str = addr.strip_prefix("0x").unwrap_or(addr);
262 let bytes = hex::decode(hex_str).map_err(|e| format!("Invalid hex address: {}", e))?;
263 if bytes.len() != 20 {
264 return Err(format!(
265 "Address must be 20 bytes, got {} bytes",
266 bytes.len()
267 ));
268 }
269 let mut arr = [0u8; 20];
270 arr.copy_from_slice(&bytes);
271 Ok(arr)
272}
273
274pub fn parse_private_key(key: &str) -> Result<[u8; 32], String> {
276 let hex_str = key.strip_prefix("0x").unwrap_or(key);
277 let bytes = hex::decode(hex_str).map_err(|e| format!("Invalid hex key: {}", e))?;
278 if bytes.len() != 32 {
279 return Err(format!(
280 "Private key must be 32 bytes, got {} bytes",
281 bytes.len()
282 ));
283 }
284 let mut arr = [0u8; 32];
285 arr.copy_from_slice(&bytes);
286 Ok(arr)
287}
288
289pub fn is_valid_address(addr: &str) -> bool {
291 addr.len() == 42 && addr.starts_with("0x") && addr[2..].chars().all(|ch| ch.is_ascii_hexdigit())
292}
293
294pub struct Eip712Signer {
301 signing_key: SigningKey,
302 address: String,
304 domain_separator: [u8; 32],
305}
306
307impl Eip712Signer {
308 pub fn new(private_key: &str, verifying_contract: &str) -> Result<Self, String> {
315 let key_bytes = parse_private_key(private_key)?;
316 let signing_key = SigningKey::from_slice(&key_bytes)
317 .map_err(|e| format!("Invalid private key: {}", e))?;
318 let verifying_contract_bytes = parse_address(verifying_contract)?;
319 let domain_separator = domain_separator(&verifying_contract_bytes);
320
321 let verifying_key = signing_key.verifying_key();
323 let encoded = verifying_key.to_encoded_point(false);
324 let public_key_bytes = encoded.as_bytes();
325 let hash = keccak256(&public_key_bytes[1..]);
326 let address = checksum_address(&hash[12..]);
327
328 Ok(Self {
329 signing_key,
330 address,
331 domain_separator,
332 })
333 }
334
335 pub fn wallet_address(&self) -> &str {
337 &self.address
338 }
339
340 pub fn sign_hash(&self, order_hash: &[u8; 32]) -> Result<String, String> {
344 let message_hash = eip712_message_hash(&self.domain_separator, order_hash);
345
346 let (sig, recovery_id) = self
348 .signing_key
349 .sign_prehash_recoverable(&message_hash)
350 .map_err(|e| format!("Signing failed: {}", e))?;
351
352 let sig = sig.normalize_s().unwrap_or(sig);
354
355 let mut sig_bytes = Vec::with_capacity(65);
357 sig_bytes.extend_from_slice(&sig.to_bytes());
358 sig_bytes.push(recovery_id.to_byte() + 27); Ok(format!("0x{}", hex::encode(&sig_bytes)))
361 }
362
363 pub fn build_gtc_order(
376 &self,
377 maker_address: &str,
378 token_id: &str,
379 side: crate::models::order::OrderSide,
380 price: f64,
381 size: f64,
382 fee_rate_bps: i32,
383 ) -> Result<crate::models::order::OrderData, String> {
384 use crate::models::order::{gtc_amounts, validate_gtc_order};
385
386 if !self.address.eq_ignore_ascii_case(maker_address) {
388 return Err(format!(
389 "wallet address mismatch: signing with '{}' but maker is '{}'",
390 self.address, maker_address
391 ));
392 }
393
394 validate_gtc_order(price, size, None)?;
396
397 let maker = parse_address(maker_address)?;
398 let taker = [0u8; 20]; let (maker_amount, taker_amount) = gtc_amounts(side, price, size);
400
401 let salt = generate_salt();
402 let expiration = "0".to_string(); let order_hash = hash_order(
405 salt,
406 &maker,
407 &maker, &taker,
409 token_id,
410 maker_amount,
411 taker_amount,
412 &expiration,
413 0, fee_rate_bps,
415 side.to_u8(),
416 0, )?;
418
419 let signature = self.sign_hash(&order_hash)?;
420
421 Ok(crate::models::order::OrderData {
422 salt,
423 maker: format!("0x{}", hex::encode(maker)),
424 signer: format!("0x{}", hex::encode(maker)),
425 taker: format!("0x{}", hex::encode(taker)),
426 token_id: token_id.to_string(),
427 maker_amount,
428 taker_amount,
429 expiration,
430 nonce: 0,
431 fee_rate_bps,
432 side: side.to_u8(),
433 signature,
434 signature_type: 0,
435 })
436 }
437
438 pub fn build_fok_order(
440 &self,
441 maker_address: &str,
442 token_id: &str,
443 side: crate::models::order::OrderSide,
444 amount: f64,
445 fee_rate_bps: i32,
446 ) -> Result<crate::models::order::OrderData, String> {
447 use crate::models::order::{fok_amount, validate_fok_order};
448
449 if !self.address.eq_ignore_ascii_case(maker_address) {
451 return Err(format!(
452 "wallet address mismatch: signing with '{}' but maker is '{}'",
453 self.address, maker_address
454 ));
455 }
456
457 validate_fok_order(amount)?;
459
460 let maker = parse_address(maker_address)?;
461 let taker = [0u8; 20];
462 let maker_amount = fok_amount(side, amount);
463
464 let salt = generate_salt();
465 let expiration = "0".to_string();
466
467 let order_hash = hash_order(
468 salt,
469 &maker,
470 &maker,
471 &taker,
472 token_id,
473 maker_amount,
474 1, &expiration,
476 0, fee_rate_bps,
478 side.to_u8(),
479 0,
480 )?;
481
482 let signature = self.sign_hash(&order_hash)?;
483
484 Ok(crate::models::order::OrderData {
485 salt,
486 maker: format!("0x{}", hex::encode(maker)),
487 signer: format!("0x{}", hex::encode(maker)),
488 taker: format!("0x{}", hex::encode(taker)),
489 token_id: token_id.to_string(),
490 maker_amount,
491 taker_amount: 1,
492 expiration,
493 nonce: 0,
494 fee_rate_bps,
495 side: side.to_u8(),
496 signature,
497 signature_type: 0,
498 })
499 }
500}
501
502pub fn checksum_address(addr: &[u8]) -> String {
506 let hex_addr = hex::encode(addr);
507 let hash = keccak256(hex_addr.as_bytes());
508
509 let mut result = String::with_capacity(42);
510 result.push_str("0x");
511 for (i, ch) in hex_addr.chars().enumerate() {
512 let hash_byte = hash[i / 2];
513 let high_nibble = (hash_byte >> 4) & 0x0f;
514 let low_nibble = if i % 2 == 0 {
515 high_nibble
516 } else {
517 hash_byte & 0x0f
518 };
519 if low_nibble >= 8 {
520 result.push(ch.to_ascii_uppercase());
521 } else {
522 result.push(ch);
523 }
524 }
525 result
526}
527
528#[cfg(test)]
531mod tests {
532 use super::*;
533
534 #[test]
535 fn test_parse_address() {
536 let addr = parse_address("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed").unwrap();
537 assert_eq!(
538 hex::encode(addr),
539 "5aaeb6053f3e94c9b9a09f33669435e7ef1beaed"
540 );
541 }
542
543 #[test]
544 fn test_parse_address_no_prefix() {
545 let addr = parse_address("5aaeb6053f3e94c9b9a09f33669435e7ef1beaed").unwrap();
546 assert_eq!(
547 hex::encode(addr),
548 "5aaeb6053f3e94c9b9a09f33669435e7ef1beaed"
549 );
550 }
551
552 #[test]
553 fn test_parse_private_key() {
554 let key =
555 parse_private_key("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80")
556 .unwrap();
557 assert_eq!(key.len(), 32);
558 }
559
560 #[test]
561 fn test_domain_separator_is_deterministic() {
562 let contract = [0x11u8; 20];
563 let ds1 = domain_separator(&contract);
564 let ds2 = domain_separator(&contract);
565 assert_eq!(ds1, ds2);
566 assert!(ds1.iter().any(|b| *b != 0));
568 }
569
570 #[test]
571 fn test_signer_derives_wallet_address() {
572 let signer = Eip712Signer::new(
574 "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
575 "0x5FbDB2315678afecb367f032d93F642f64180aa3", )
577 .unwrap();
578
579 let address = signer.wallet_address();
580 assert_eq!(
582 address.to_lowercase(),
583 "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"
584 );
585 }
586
587 #[test]
588 fn test_eip712_hash_is_deterministic() {
589 let maker = [0x11u8; 20];
590 let signer = [0x11u8; 20];
591 let taker = [0u8; 20];
592
593 let h1 = hash_order(
594 12345, &maker, &signer, &taker, "1000000", 5000000, 10000000, "0", 42, 0, 0, 0,
595 )
596 .unwrap();
597 let h2 = hash_order(
598 12345, &maker, &signer, &taker, "1000000", 5000000, 10000000, "0", 42, 0, 0, 0,
599 )
600 .unwrap();
601 assert_eq!(h1, h2);
602 }
603
604 #[test]
605 fn test_encode_decimal_string_as_u256() {
606 let result = encode_decimal_string_as_u256("1000000").unwrap();
608 assert_eq!(result[31], 64); assert_eq!(result[30], 66);
610 assert_eq!(result[29], 15);
611 }
612
613 #[test]
614 fn test_generate_salt_is_monotonic() {
615 let s1 = generate_salt();
616 let s2 = generate_salt();
617 assert!(s2 > s1, "s2={s2} must be greater than s1={s1}");
618 }
619
620 #[test]
621 fn test_chain_id_encoding_is_full_u256() {
622 let encoded = encode_u256_from_u64(CHAIN_ID);
623 assert_eq!(encoded[30], 0x21);
625 assert_eq!(encoded[31], 0x05);
626 assert_eq!(encoded[24], 0);
628 assert_eq!(encoded[27], 0);
629 }
630}