clob_client_rust/
signer_adapter.rs

1use crate::errors::ClobError;
2use crate::signing::Eip712Signer;
3use alloy::primitives::{Address, U256};
4use alloy::signers::Signer;
5use alloy::signers::local::PrivateKeySigner;
6use alloy::sol;
7use alloy::sol_types::{Eip712Domain, SolStruct};
8use async_trait::async_trait;
9use std::str::FromStr;
10use std::sync::Arc;
11
12// Define Sol structs matching the JSON structure
13sol! {
14    #[derive(serde::Deserialize, Debug)]
15    struct ClobAuth {
16        address address;
17        string timestamp;
18        uint256 nonce;
19        string message;
20    }
21
22    #[derive(serde::Deserialize, Debug)]
23    struct Order {
24        uint256 salt;
25        address maker;
26        address signer;
27        address taker;
28        uint256 tokenId;
29        uint256 makerAmount;
30        uint256 takerAmount;
31        uint256 expiration;
32        uint256 nonce;
33        uint256 feeRateBps;
34        uint8 side;
35        uint8 signatureType;
36    }
37}
38
39/// A simple alloy-based signer adapter that implements Eip712Signer.
40pub struct EthersSigner {
41    wallet: Arc<PrivateKeySigner>,
42}
43
44impl EthersSigner {
45    /// Create a new EthersSigner from a hex private key string (with or without 0x).
46    pub fn new_from_private_key(hex_priv: &str) -> Result<Self, ClobError> {
47        let w = PrivateKeySigner::from_str(hex_priv)
48            .map_err(|e| ClobError::Other(format!("invalid private key: {}", e)))?;
49        Ok(Self {
50            wallet: Arc::new(w),
51        })
52    }
53
54    /// Return inner wallet (cloneable Arc) for advanced usage
55    pub fn wallet(&self) -> Arc<PrivateKeySigner> {
56        self.wallet.clone()
57    }
58}
59
60#[async_trait]
61impl Eip712Signer for EthersSigner {
62    async fn get_address(&self) -> Result<String, ClobError> {
63        // Produce a full hex address explicitly (0x-prefixed)
64        Ok(format!("{:#x}", self.wallet.address()))
65    }
66
67    async fn sign_typed_data(
68        &self,
69        domain: &str,
70        types: &str,
71        value: &str,
72    ) -> Result<String, ClobError> {
73        // Parse domain JSON
74        let domain_val: serde_json::Value = serde_json::from_str(domain)
75            .map_err(|e| ClobError::Other(format!("invalid domain json: {}", e)))?;
76
77        let name = domain_val
78            .get("name")
79            .and_then(|v| v.as_str())
80            .map(|s| s.to_string().into());
81        let version = domain_val
82            .get("version")
83            .and_then(|v| v.as_str())
84            .map(|s| s.to_string().into());
85        let chain_id = domain_val
86            .get("chainId")
87            .and_then(|v| v.as_u64())
88            .map(|v| U256::from(v));
89        let verifying_contract = domain_val
90            .get("verifyingContract")
91            .and_then(|v| v.as_str())
92            .map(|s| Address::from_str(s).unwrap_or_default());
93
94        let domain_struct = Eip712Domain {
95            name,
96            version,
97            chain_id,
98            verifying_contract,
99            salt: None,
100        };
101
102        // Determine primary type from types JSON
103        let types_val: serde_json::Value = serde_json::from_str(types)
104            .map_err(|e| ClobError::Other(format!("invalid types json: {}", e)))?;
105
106        let primary_type = if let Some(map) = types_val.as_object() {
107            if map.contains_key("ClobAuth") {
108                "ClobAuth"
109            } else {
110                "Order"
111            }
112        } else {
113            "Order"
114        };
115
116        let sig = match primary_type {
117            "ClobAuth" => {
118                let auth: ClobAuth = serde_json::from_str(value)
119                    .map_err(|e| ClobError::Other(format!("failed to parse ClobAuth: {}", e)))?;
120                let hash = auth.eip712_signing_hash(&domain_struct);
121                self.wallet.sign_hash(&hash).await
122            }
123            "Order" => {
124                let order: Order = serde_json::from_str(value)
125                    .map_err(|e| ClobError::Other(format!("failed to parse Order: {}", e)))?;
126                let hash = order.eip712_signing_hash(&domain_struct);
127                self.wallet.sign_hash(&hash).await
128            }
129            _ => {
130                return Err(ClobError::Other(format!(
131                    "unknown primary type: {}",
132                    primary_type
133                )));
134            }
135        };
136
137        match sig {
138            Ok(s) => {
139                // Alloy signature as_bytes() returns 65 bytes [r, s, v]
140                // Check v. If it's 0/1, convert to 27/28 for compatibility if needed.
141                // Ethers usually produces 27/28.
142                let mut bytes = s.as_bytes();
143                if bytes[64] < 27 {
144                    bytes[64] += 27;
145                }
146                Ok(hex::encode(bytes))
147            }
148            Err(e) => Err(ClobError::Other(format!("sign error: {}", e))),
149        }
150    }
151}