Skip to main content

polyfill_rs/
auth.rs

1//! Authentication and cryptographic utilities for Polymarket API
2//!
3//! This module provides EIP-712 signing, HMAC authentication, and header generation
4//! for secure communication with the Polymarket CLOB API.
5
6use crate::errors::{PolyfillError, Result};
7use crate::types::ApiCredentials;
8use alloy_primitives::{hex::encode_prefixed, Address, B256, U256};
9use alloy_signer::SignerSync;
10use alloy_signer_local::PrivateKeySigner;
11use alloy_sol_types::{eip712_domain, sol, Eip712Domain, SolStruct};
12use base64::engine::Engine;
13use hmac::{Hmac, Mac};
14use serde::Serialize;
15use sha2::Sha256;
16use std::borrow::Cow;
17use std::collections::HashMap;
18use std::ops::Deref;
19use std::sync::Arc;
20use std::time::{SystemTime, UNIX_EPOCH};
21
22// Header constants
23const POLY_ADDR_HEADER: &str = "poly_address";
24const POLY_SIG_HEADER: &str = "poly_signature";
25const POLY_TS_HEADER: &str = "poly_timestamp";
26const POLY_NONCE_HEADER: &str = "poly_nonce";
27const POLY_API_KEY_HEADER: &str = "poly_api_key";
28const POLY_PASS_HEADER: &str = "poly_passphrase";
29
30type Headers = HashMap<&'static str, String>;
31
32pub trait HmacApiCredentials {
33    fn api_key(&self) -> &str;
34    fn passphrase(&self) -> &str;
35    fn decoded_secret_bytes(&self) -> Result<Cow<'_, [u8]>>;
36}
37
38#[derive(Debug, Clone)]
39pub struct PreparedApiCredentials {
40    credentials: ApiCredentials,
41    decoded_secret: Arc<[u8]>,
42}
43
44impl PreparedApiCredentials {
45    pub fn try_new(credentials: ApiCredentials) -> Result<Self> {
46        let decoded_secret = base64::engine::general_purpose::URL_SAFE
47            .decode(&credentials.secret)
48            .map(Into::into)
49            .map_err(|e| PolyfillError::crypto(format!("Failed to decode base64 secret: {e}")))?;
50
51        Ok(Self {
52            credentials,
53            decoded_secret,
54        })
55    }
56
57    pub fn credentials(&self) -> &ApiCredentials {
58        &self.credentials
59    }
60}
61
62impl Deref for PreparedApiCredentials {
63    type Target = ApiCredentials;
64
65    fn deref(&self) -> &Self::Target {
66        &self.credentials
67    }
68}
69
70impl HmacApiCredentials for ApiCredentials {
71    fn api_key(&self) -> &str {
72        &self.api_key
73    }
74
75    fn passphrase(&self) -> &str {
76        &self.passphrase
77    }
78
79    fn decoded_secret_bytes(&self) -> Result<Cow<'_, [u8]>> {
80        Ok(Cow::Owned(decode_secret_bytes(&self.secret)?))
81    }
82}
83
84impl HmacApiCredentials for PreparedApiCredentials {
85    fn api_key(&self) -> &str {
86        &self.credentials.api_key
87    }
88
89    fn passphrase(&self) -> &str {
90        &self.credentials.passphrase
91    }
92
93    fn decoded_secret_bytes(&self) -> Result<Cow<'_, [u8]>> {
94        Ok(Cow::Borrowed(self.decoded_secret.as_ref()))
95    }
96}
97
98// EIP-712 struct for CLOB authentication
99sol! {
100    struct ClobAuth {
101        address address;
102        string timestamp;
103        uint256 nonce;
104        string message;
105    }
106}
107
108// EIP-712 struct for order signing
109sol! {
110    struct Order {
111        uint256 salt;
112        address maker;
113        address signer;
114        uint256 tokenId;
115        uint256 makerAmount;
116        uint256 takerAmount;
117        uint8 side;
118        uint8 signatureType;
119        uint256 timestamp;
120        bytes32 metadata;
121        bytes32 builder;
122    }
123
124    struct TypedDataSign {
125        Order contents;
126        string name;
127        string version;
128        uint256 chainId;
129        address verifyingContract;
130        bytes32 salt;
131    }
132}
133
134const DEPOSIT_WALLET_NAME: &str = "DepositWallet";
135const DEPOSIT_WALLET_VERSION: &str = "1";
136const ERC7739_TYPED_DATA_SIGN_SALT: B256 = B256::ZERO;
137const ORDER_TYPE_STRING: &str = "Order(uint256 salt,address maker,address signer,uint256 tokenId,uint256 makerAmount,uint256 takerAmount,uint8 side,uint8 signatureType,uint256 timestamp,bytes32 metadata,bytes32 builder)";
138
139/// V2 order signing payload. The REST body still carries `expiration`, but the EIP-712 payload
140/// follows the V2 exchange struct.
141#[derive(Clone)]
142pub struct SignedOrderMessage {
143    pub salt: U256,
144    pub maker: Address,
145    pub signer: Address,
146    pub token_id: U256,
147    pub maker_amount: U256,
148    pub taker_amount: U256,
149    pub side: u8,
150    pub signature_type: u8,
151    pub timestamp: U256,
152    pub metadata: B256,
153    pub builder: B256,
154}
155
156/// Prepared EIP-712 domain for signing orders against one exchange contract.
157#[derive(Clone)]
158pub struct PreparedOrderDomain {
159    domain: Eip712Domain,
160}
161
162impl PreparedOrderDomain {
163    pub fn new(chain_id: u64, verifying_contract: Address) -> Self {
164        let domain = eip712_domain!(
165            name: "Polymarket CTF Exchange",
166            version: "2",
167            chain_id: chain_id,
168            verifying_contract: verifying_contract,
169        );
170
171        Self { domain }
172    }
173}
174
175/// Get current Unix timestamp in seconds
176pub fn get_current_unix_time_secs() -> u64 {
177    SystemTime::now()
178        .duration_since(UNIX_EPOCH)
179        .expect("Time went backwards")
180        .as_secs()
181}
182
183/// Sign CLOB authentication message using EIP-712
184pub fn sign_clob_auth_message(
185    signer: &PrivateKeySigner,
186    timestamp: String,
187    nonce: U256,
188) -> Result<String> {
189    let message = "This message attests that I control the given wallet".to_string();
190    let polygon = 137;
191
192    let auth_struct = ClobAuth {
193        address: signer.address(),
194        timestamp,
195        nonce,
196        message,
197    };
198
199    let domain = eip712_domain!(
200        name: "ClobAuthDomain",
201        version: "1",
202        chain_id: polygon,
203    );
204
205    let signature = signer
206        .sign_typed_data_sync(&auth_struct, &domain)
207        .map_err(|e| PolyfillError::crypto(format!("EIP-712 signature failed: {}", e)))?;
208
209    Ok(encode_prefixed(signature.as_bytes()))
210}
211
212/// Sign order message using EIP-712
213pub fn sign_order_message(
214    signer: &PrivateKeySigner,
215    order: SignedOrderMessage,
216    chain_id: u64,
217    verifying_contract: Address,
218) -> Result<String> {
219    let domain = PreparedOrderDomain::new(chain_id, verifying_contract);
220    sign_order_message_with_domain(signer, order, &domain)
221}
222
223/// Sign order message using a prepared EIP-712 domain.
224pub fn sign_order_message_with_domain(
225    signer: &PrivateKeySigner,
226    order: SignedOrderMessage,
227    domain: &PreparedOrderDomain,
228) -> Result<String> {
229    let order = order_sol(order);
230
231    let signature = signer
232        .sign_typed_data_sync(&order, &domain.domain)
233        .map_err(|e| PolyfillError::crypto(format!("Order signature failed: {}", e)))?;
234
235    Ok(encode_prefixed(signature.as_bytes()))
236}
237
238/// Sign a POLY_1271 deposit-wallet order using the ERC-7739 wrapper expected by
239/// Polymarket's V2 deposit wallet verifier.
240///
241/// The order itself must have `maker == signer == deposit_wallet` and
242/// `signatureType == 3`. The EOA key signs a Solady `TypedDataSign(...)`
243/// envelope under the CTF exchange order domain; the wire signature appends the
244/// app-domain separator, order contents hash, order type string, and a uint16
245/// big-endian type-string length.
246pub fn sign_poly1271_order_message_with_domain(
247    signer: &PrivateKeySigner,
248    order: SignedOrderMessage,
249    domain: &PreparedOrderDomain,
250    deposit_wallet: Address,
251    chain_id: u64,
252) -> Result<String> {
253    if order.maker != deposit_wallet || order.signer != deposit_wallet {
254        return Err(PolyfillError::validation(
255            "POLY_1271 orders require maker and signer to equal the deposit wallet",
256        ));
257    }
258
259    let order = order_sol(order);
260    let app_domain_separator = domain.domain.hash_struct();
261    let contents_hash = order.eip712_hash_struct();
262    let envelope = TypedDataSign {
263        contents: order,
264        name: DEPOSIT_WALLET_NAME.to_string(),
265        version: DEPOSIT_WALLET_VERSION.to_string(),
266        chainId: U256::from(chain_id),
267        verifyingContract: deposit_wallet,
268        salt: ERC7739_TYPED_DATA_SIGN_SALT,
269    };
270
271    let inner = signer
272        .sign_typed_data_sync(&envelope, &domain.domain)
273        .map_err(|e| PolyfillError::crypto(format!("POLY_1271 order signature failed: {e}")))?;
274
275    let type_bytes = ORDER_TYPE_STRING.as_bytes();
276    let type_len: u16 = type_bytes
277        .len()
278        .try_into()
279        .map_err(|_| PolyfillError::validation("POLY_1271 order type string too long"))?;
280    let mut wrapped = Vec::with_capacity(65 + 32 + 32 + type_bytes.len() + 2);
281    wrapped.extend_from_slice(&inner.as_bytes());
282    wrapped.extend_from_slice(app_domain_separator.as_slice());
283    wrapped.extend_from_slice(contents_hash.as_slice());
284    wrapped.extend_from_slice(type_bytes);
285    wrapped.extend_from_slice(&type_len.to_be_bytes());
286
287    Ok(encode_prefixed(wrapped))
288}
289
290fn order_sol(order: SignedOrderMessage) -> Order {
291    Order {
292        salt: order.salt,
293        maker: order.maker,
294        signer: order.signer,
295        tokenId: order.token_id,
296        makerAmount: order.maker_amount,
297        takerAmount: order.taker_amount,
298        side: order.side,
299        signatureType: order.signature_type,
300        timestamp: order.timestamp,
301        metadata: order.metadata,
302        builder: order.builder,
303    }
304}
305
306/// Build HMAC signature for L2 authentication
307///
308/// Performs cryptographic message authentication using SHA-256 with
309/// specialized key derivation and encoding schemes for API compliance.
310pub fn build_hmac_signature<T>(
311    secret: &str,
312    timestamp: u64,
313    method: &str,
314    request_path: &str,
315    body: Option<&T>,
316) -> Result<String>
317where
318    T: ?Sized + Serialize,
319{
320    let decoded_secret = decode_secret_bytes(secret)?;
321    let body_bytes =
322        match body {
323            Some(b) => Some(serde_json::to_vec(b).map_err(|e| {
324                PolyfillError::parse(format!("Failed to serialize body: {}", e), None)
325            })?),
326            None => None,
327        };
328
329    build_hmac_signature_bytes(
330        &decoded_secret,
331        timestamp,
332        method,
333        request_path,
334        body_bytes.as_deref(),
335    )
336}
337
338pub fn build_hmac_signature_bytes(
339    decoded_secret: &[u8],
340    timestamp: u64,
341    method: &str,
342    request_path: &str,
343    body_bytes: Option<&[u8]>,
344) -> Result<String> {
345    let mut mac = Hmac::<Sha256>::new_from_slice(decoded_secret)
346        .map_err(|e| PolyfillError::crypto(format!("Invalid HMAC key: {}", e)))?;
347
348    let timestamp = timestamp.to_string();
349    mac.update(timestamp.as_bytes());
350    let method_upper;
351    let method_bytes = if method.bytes().all(|b| !b.is_ascii_lowercase()) {
352        method.as_bytes()
353    } else {
354        method_upper = method.to_ascii_uppercase();
355        method_upper.as_bytes()
356    };
357    mac.update(method_bytes);
358    mac.update(request_path.as_bytes());
359    if let Some(body_bytes) = body_bytes {
360        mac.update(body_bytes);
361    }
362
363    let result = mac.finalize();
364
365    Ok(base64::engine::general_purpose::URL_SAFE.encode(result.into_bytes()))
366}
367
368fn decode_secret_bytes(secret: &str) -> Result<Vec<u8>> {
369    base64::engine::general_purpose::URL_SAFE
370        .decode(secret)
371        .map_err(|e| PolyfillError::crypto(format!("Failed to decode base64 secret: {}", e)))
372}
373
374/// Create L1 headers for authentication (using private key signature)
375///
376/// Generates initial authentication envelope using elliptic curve cryptography
377/// for establishing trusted communication channels with the distributed ledger API.
378pub fn create_l1_headers(signer: &PrivateKeySigner, nonce: Option<U256>) -> Result<Headers> {
379    // Capture temporal context for replay prevention at protocol boundary
380    let timestamp = get_current_unix_time_secs().to_string();
381    let nonce = nonce.unwrap_or(U256::ZERO);
382
383    // Generate EIP-712 compliant signature for cryptographic proof of authority
384    let signature = sign_clob_auth_message(signer, timestamp.clone(), nonce)?;
385    let address = encode_prefixed(signer.address().as_slice());
386
387    // Assemble primary authentication header set with identity binding
388    Ok(HashMap::from([
389        (POLY_ADDR_HEADER, address),
390        (POLY_SIG_HEADER, signature),
391        (POLY_TS_HEADER, timestamp),
392        (POLY_NONCE_HEADER, nonce.to_string()),
393    ]))
394}
395
396/// Create L2 headers for API calls (using API key and HMAC)
397///
398/// Assembles authentication header set with computed signature digest
399/// to satisfy bilateral verification requirements at the protocol layer.
400pub fn create_l2_headers<T>(
401    signer: &PrivateKeySigner,
402    api_creds: &(impl HmacApiCredentials + ?Sized),
403    method: &str,
404    req_path: &str,
405    body: Option<&T>,
406) -> Result<Headers>
407where
408    T: ?Sized + Serialize,
409{
410    // Extract identity from signing authority for header binding
411    let address = encode_prefixed(signer.address().as_slice());
412    let timestamp = get_current_unix_time_secs();
413
414    // Generate cryptographic authenticator using temporal and message context
415    let decoded_secret = api_creds.decoded_secret_bytes()?;
416    let body_bytes =
417        match body {
418            Some(b) => Some(serde_json::to_vec(b).map_err(|e| {
419                PolyfillError::parse(format!("Failed to serialize body: {}", e), None)
420            })?),
421            None => None,
422        };
423    let hmac_signature = build_hmac_signature_bytes(
424        &decoded_secret,
425        timestamp,
426        method,
427        req_path,
428        body_bytes.as_deref(),
429    )?;
430
431    // Construct header map with authentication primitives in canonical order
432    Ok(HashMap::from([
433        (POLY_ADDR_HEADER, address),
434        (POLY_SIG_HEADER, hmac_signature),
435        (POLY_TS_HEADER, timestamp.to_string()),
436        (POLY_API_KEY_HEADER, api_creds.api_key().to_string()),
437        (POLY_PASS_HEADER, api_creds.passphrase().to_string()),
438    ]))
439}
440
441pub fn create_l2_headers_with_body_bytes(
442    signer: &PrivateKeySigner,
443    api_creds: &(impl HmacApiCredentials + ?Sized),
444    method: &str,
445    req_path: &str,
446    body_bytes: Option<&[u8]>,
447) -> Result<Headers> {
448    let address = encode_prefixed(signer.address().as_slice());
449    let timestamp = get_current_unix_time_secs();
450    let decoded_secret = api_creds.decoded_secret_bytes()?;
451    let hmac_signature =
452        build_hmac_signature_bytes(&decoded_secret, timestamp, method, req_path, body_bytes)?;
453
454    Ok(HashMap::from([
455        (POLY_ADDR_HEADER, address),
456        (POLY_SIG_HEADER, hmac_signature),
457        (POLY_TS_HEADER, timestamp.to_string()),
458        (POLY_API_KEY_HEADER, api_creds.api_key().to_string()),
459        (POLY_PASS_HEADER, api_creds.passphrase().to_string()),
460    ]))
461}
462
463#[cfg(test)]
464mod tests {
465    use super::*;
466    use std::str::FromStr;
467
468    #[test]
469    fn test_unix_timestamp() {
470        let timestamp = get_current_unix_time_secs();
471        assert!(timestamp > 1_600_000_000); // Should be after 2020
472    }
473
474    #[test]
475    fn test_hmac_signature() {
476        let result = build_hmac_signature::<String>(
477            "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1",
478            1234567890,
479            "GET",
480            "/test",
481            None,
482        );
483        assert!(result.is_ok());
484    }
485
486    #[test]
487    fn test_hmac_signature_with_body() {
488        let body = r#"{"test": "data"}"#;
489        let result = build_hmac_signature(
490            "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1",
491            1234567890,
492            "POST",
493            "/orders",
494            Some(body),
495        );
496        assert!(result.is_ok());
497        let signature = result.unwrap();
498        assert!(!signature.is_empty());
499    }
500
501    #[test]
502    fn test_hmac_signature_consistency() {
503        let secret = "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1";
504        let timestamp = 1234567890;
505        let method = "GET";
506        let path = "/test";
507
508        let sig1 = build_hmac_signature::<String>(secret, timestamp, method, path, None).unwrap();
509        let sig2 = build_hmac_signature::<String>(secret, timestamp, method, path, None).unwrap();
510
511        // Same inputs should produce same signature
512        assert_eq!(sig1, sig2);
513    }
514
515    #[test]
516    fn test_hmac_signature_bytes_matches_serialized_body() {
517        let secret = "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1";
518        let timestamp = 1234567890;
519        let body = serde_json::json!({"orderID": "abc123"});
520        let body_bytes = serde_json::to_vec(&body).unwrap();
521        let decoded_secret = decode_secret_bytes(secret).unwrap();
522
523        let object_signature =
524            build_hmac_signature(secret, timestamp, "delete", "/order", Some(&body)).unwrap();
525        let bytes_signature = build_hmac_signature_bytes(
526            &decoded_secret,
527            timestamp,
528            "DELETE",
529            "/order",
530            Some(&body_bytes),
531        )
532        .unwrap();
533
534        assert_eq!(object_signature, bytes_signature);
535    }
536
537    #[test]
538    fn test_hmac_signature_different_inputs() {
539        let secret = "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1";
540        let timestamp = 1234567890;
541
542        let sig1 = build_hmac_signature::<String>(secret, timestamp, "GET", "/test", None).unwrap();
543        let sig2 =
544            build_hmac_signature::<String>(secret, timestamp, "POST", "/test", None).unwrap();
545        let sig3 =
546            build_hmac_signature::<String>(secret, timestamp, "GET", "/other", None).unwrap();
547
548        // Different inputs should produce different signatures
549        assert_ne!(sig1, sig2);
550        assert_ne!(sig1, sig3);
551        assert_ne!(sig2, sig3);
552    }
553
554    #[test]
555    fn test_create_l1_headers() {
556        use alloy_primitives::U256;
557        use alloy_signer_local::PrivateKeySigner;
558
559        let private_key = "0x1234567890123456789012345678901234567890123456789012345678901234";
560        let signer: PrivateKeySigner = private_key.parse().expect("Valid private key");
561
562        let result = create_l1_headers(&signer, Some(U256::from(12345)));
563        assert!(result.is_ok());
564
565        let headers = result.unwrap();
566        assert!(headers.contains_key("poly_address"));
567        assert!(headers.contains_key("poly_signature"));
568        assert!(headers.contains_key("poly_timestamp"));
569        assert!(headers.contains_key("poly_nonce"));
570    }
571
572    #[test]
573    fn test_create_l1_headers_different_nonces() {
574        use alloy_primitives::U256;
575        use alloy_signer_local::PrivateKeySigner;
576
577        let private_key = "0x1234567890123456789012345678901234567890123456789012345678901234";
578        let signer: PrivateKeySigner = private_key.parse().expect("Valid private key");
579
580        let headers_1 = create_l1_headers(&signer, Some(U256::from(12345))).unwrap();
581        let headers_2 = create_l1_headers(&signer, Some(U256::from(54321))).unwrap();
582
583        // Different nonces should produce different signatures
584        assert_ne!(
585            headers_1.get("poly_signature"),
586            headers_2.get("poly_signature")
587        );
588
589        // But same address
590        assert_eq!(headers_1.get("poly_address"), headers_2.get("poly_address"));
591    }
592
593    #[test]
594    fn test_create_l2_headers() {
595        use alloy_signer_local::PrivateKeySigner;
596
597        let private_key = "0x1234567890123456789012345678901234567890123456789012345678901234";
598        let signer: PrivateKeySigner = private_key.parse().expect("Valid private key");
599
600        let api_creds = ApiCredentials {
601            api_key: "test_key".to_string(),
602            secret: "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1".to_string(),
603            passphrase: "test_passphrase".to_string(),
604        };
605
606        let result = create_l2_headers::<String>(&signer, &api_creds, "GET", "/test", None);
607        assert!(result.is_ok());
608
609        let headers = result.unwrap();
610        assert!(headers.contains_key("poly_api_key"));
611        assert!(headers.contains_key("poly_signature"));
612        assert!(headers.contains_key("poly_timestamp"));
613        assert!(headers.contains_key("poly_passphrase"));
614
615        assert_eq!(headers.get("poly_api_key").unwrap(), "test_key");
616        assert_eq!(headers.get("poly_passphrase").unwrap(), "test_passphrase");
617    }
618
619    #[test]
620    fn test_eip712_signature_format() {
621        use alloy_primitives::U256;
622        use alloy_signer_local::PrivateKeySigner;
623
624        let private_key = "0x1234567890123456789012345678901234567890123456789012345678901234";
625        let signer: PrivateKeySigner = private_key.parse().expect("Valid private key");
626
627        // Test that we can create and sign EIP-712 messages
628        let result = create_l1_headers(&signer, Some(U256::from(12345)));
629        assert!(result.is_ok());
630
631        let headers = result.unwrap();
632        let signature = headers.get("poly_signature").unwrap();
633
634        // EIP-712 signatures should be hex strings of specific length
635        assert!(signature.starts_with("0x"));
636        assert_eq!(signature.len(), 132); // 0x + 130 hex chars = 132 total
637    }
638
639    #[test]
640    fn test_poly1271_wrapped_order_signature_golden() {
641        let signer: PrivateKeySigner =
642            "0x2222222222222222222222222222222222222222222222222222222222222222"
643                .parse()
644                .expect("valid private key");
645        let deposit_wallet =
646            Address::from_str("0x000000000000000000000000000000000000d077").unwrap();
647        let exchange = Address::from_str("0xE111180000d2663C0091e4f400237545B87B996B").unwrap();
648        let order = SignedOrderMessage {
649            salt: U256::from(12345),
650            maker: deposit_wallet,
651            signer: deposit_wallet,
652            token_id: U256::from_str_radix(
653                "100000000000000000000000000000000000000000000000000000000000000000000000000000",
654                10,
655            )
656            .unwrap(),
657            maker_amount: U256::from(1_000_000),
658            taker_amount: U256::from(5_000_000),
659            side: 0,
660            signature_type: 3,
661            timestamp: U256::from(1_716_000_000_000_u64),
662            metadata: B256::ZERO,
663            builder: B256::ZERO,
664        };
665
666        let got = sign_poly1271_order_message_with_domain(
667            &signer,
668            order,
669            &PreparedOrderDomain::new(137, exchange),
670            deposit_wallet,
671            137,
672        )
673        .unwrap();
674
675        const WANT: &str = "0xc1c199d3822d7f465b1f213793ebb7bbc3a77810ceefa89b58ecfeb284e708953c5ec946a41c6fa2a6fac373c78b167dd6834fae6f3e37581111ffb06200f1d21b3264e159346253e26a64e00b69032db0e7d32f94628de3e6eecb50304d7af3d26814859c22020105275eba8b46528be2edd6078dea415bec6d90f2076b0c8ac64f726465722875696e743235362073616c742c61646472657373206d616b65722c61646472657373207369676e65722c75696e7432353620746f6b656e49642c75696e74323536206d616b6572416d6f756e742c75696e743235362074616b6572416d6f756e742c75696e743820736964652c75696e7438207369676e6174757265547970652c75696e743235362074696d657374616d702c62797465733332206d657461646174612c62797465733332206275696c6465722900ba";
676        assert_eq!(got, WANT);
677        assert!(got.len() > 600 && got.len() < 700);
678    }
679
680    #[test]
681    fn test_timestamp_generation() {
682        let ts1 = get_current_unix_time_secs();
683        std::thread::sleep(std::time::Duration::from_millis(1));
684        let ts2 = get_current_unix_time_secs();
685
686        // Timestamps should be increasing
687        assert!(ts2 >= ts1);
688
689        // Should be reasonable current time (after 2020, before 2030)
690        assert!(ts1 > 1_600_000_000);
691        assert!(ts1 < 1_900_000_000);
692    }
693}