rust_x402/crypto/
eip712.rs

1//! EIP-712 typed data utilities
2
3use crate::{Result, X402Error};
4use ethereum_types::{Address, H256, U256};
5use serde_json::json;
6use std::str::FromStr;
7
8/// EIP-712 domain separator
9#[derive(Debug, Clone)]
10pub struct Domain {
11    pub name: String,
12    pub version: String,
13    pub chain_id: u64,
14    pub verifying_contract: Address,
15}
16
17/// EIP-712 typed data structure
18#[derive(Debug, Clone)]
19pub struct TypedData {
20    pub domain: Domain,
21    pub primary_type: String,
22    pub types: serde_json::Value,
23    pub message: serde_json::Value,
24}
25
26/// Create EIP-712 hash for EIP-3009 transfer with authorization
27pub fn create_transfer_with_authorization_hash(
28    domain: &Domain,
29    from: Address,
30    to: Address,
31    value: U256,
32    valid_after: U256,
33    valid_before: U256,
34    nonce: H256,
35) -> Result<H256> {
36    let types = json!({
37        "EIP712Domain": [
38            {"name": "name", "type": "string"},
39            {"name": "version", "type": "string"},
40            {"name": "chainId", "type": "uint256"},
41            {"name": "verifyingContract", "type": "address"}
42        ],
43        "TransferWithAuthorization": [
44            {"name": "from", "type": "address"},
45            {"name": "to", "type": "address"},
46            {"name": "value", "type": "uint256"},
47            {"name": "validAfter", "type": "uint256"},
48            {"name": "validBefore", "type": "uint256"},
49            {"name": "nonce", "type": "bytes32"}
50        ]
51    });
52
53    let message = json!({
54        "from": format!("{:?}", from),
55        "to": format!("{:?}", to),
56        "value": format!("0x{:x}", value),
57        "validAfter": format!("0x{:x}", valid_after),
58        "validBefore": format!("0x{:x}", valid_before),
59        "nonce": format!("{:?}", nonce)
60    });
61
62    let typed_data = TypedData {
63        domain: domain.clone(),
64        primary_type: "TransferWithAuthorization".to_string(),
65        types,
66        message,
67    };
68
69    hash_typed_data(&typed_data)
70}
71
72/// Hash EIP-712 typed data
73pub fn hash_typed_data(typed_data: &TypedData) -> Result<H256> {
74    // Full EIP-712 implementation following the specification
75
76    let domain_separator = hash_domain(&typed_data.domain)?;
77    let struct_hash = hash_struct(
78        &typed_data.primary_type,
79        &typed_data.types,
80        &typed_data.message,
81    )?;
82
83    // EIP-712: hash(0x1901 || domain_separator || struct_hash)
84    let mut data = Vec::new();
85    data.extend_from_slice(&[0x19, 0x01]); // EIP-712 prefix
86    data.extend_from_slice(domain_separator.as_bytes());
87    data.extend_from_slice(struct_hash.as_bytes());
88
89    Ok(H256::from_slice(&keccak256(&data)))
90}
91
92/// Hash the domain separator
93fn hash_domain(domain: &Domain) -> Result<H256> {
94    let domain_type_hash = keccak256(
95        b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)",
96    );
97
98    let name_hash = keccak256(domain.name.as_bytes());
99    let version_hash = keccak256(domain.version.as_bytes());
100    let chain_id_hash = keccak256(&domain.chain_id.to_be_bytes());
101    let verifying_contract_hash = keccak256(domain.verifying_contract.as_bytes());
102
103    let mut data = Vec::new();
104    data.extend_from_slice(&domain_type_hash);
105    data.extend_from_slice(&name_hash);
106    data.extend_from_slice(&version_hash);
107    data.extend_from_slice(&chain_id_hash);
108    data.extend_from_slice(&verifying_contract_hash);
109
110    Ok(H256::from_slice(&keccak256(&data)))
111}
112
113/// Hash a struct according to EIP-712
114fn hash_struct(
115    primary_type: &str,
116    _types: &serde_json::Value,
117    message: &serde_json::Value,
118) -> Result<H256> {
119    // Full EIP-712 struct hashing implementation
120
121    // For TransferWithAuthorization, create the proper type hash
122    let type_hash = keccak256(
123        format!("{}(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)", primary_type)
124        .as_bytes()
125    );
126
127    // Encode the message fields in the correct order
128    let encoded_message = encode_message_fields(message)?;
129    let message_hash = keccak256(&encoded_message);
130
131    // Combine type hash and message hash
132    let mut data = Vec::new();
133    data.extend_from_slice(&type_hash);
134    data.extend_from_slice(&message_hash);
135
136    Ok(H256::from_slice(&keccak256(&data)))
137}
138
139/// Encode message fields for hashing
140fn encode_message_fields(message: &serde_json::Value) -> Result<Vec<u8>> {
141    // For TransferWithAuthorization, encode fields in the correct order
142    let mut encoded = Vec::new();
143
144    // Encode 'from' address (32 bytes, padded)
145    if let Some(from) = message.get("from") {
146        if let Some(addr_str) = from.as_str() {
147            let addr = Address::from_str(addr_str)
148                .map_err(|_| X402Error::invalid_authorization("Invalid from address"))?;
149            let mut padded = [0u8; 32];
150            padded[12..32].copy_from_slice(addr.as_bytes());
151            encoded.extend_from_slice(&padded);
152        }
153    }
154
155    // Encode 'to' address (32 bytes, padded)
156    if let Some(to) = message.get("to") {
157        if let Some(addr_str) = to.as_str() {
158            let addr = Address::from_str(addr_str)
159                .map_err(|_| X402Error::invalid_authorization("Invalid to address"))?;
160            let mut padded = [0u8; 32];
161            padded[12..32].copy_from_slice(addr.as_bytes());
162            encoded.extend_from_slice(&padded);
163        }
164    }
165
166    // Encode 'value' (32 bytes, big-endian)
167    if let Some(value) = message.get("value") {
168        if let Some(value_str) = value.as_str() {
169            let value_hex = value_str.trim_start_matches("0x");
170            let value_bytes = hex::decode(value_hex)
171                .map_err(|_| X402Error::invalid_authorization("Invalid value format"))?;
172            let mut padded = [0u8; 32];
173            let start = 32 - value_bytes.len();
174            padded[start..].copy_from_slice(&value_bytes);
175            encoded.extend_from_slice(&padded);
176        }
177    }
178
179    // Encode 'validAfter' (32 bytes, big-endian)
180    if let Some(valid_after) = message.get("validAfter") {
181        if let Some(valid_after_str) = valid_after.as_str() {
182            let valid_after_hex = valid_after_str.trim_start_matches("0x");
183            let valid_after_bytes = hex::decode(valid_after_hex)
184                .map_err(|_| X402Error::invalid_authorization("Invalid validAfter format"))?;
185            let mut padded = [0u8; 32];
186            let start = 32 - valid_after_bytes.len();
187            padded[start..].copy_from_slice(&valid_after_bytes);
188            encoded.extend_from_slice(&padded);
189        }
190    }
191
192    // Encode 'validBefore' (32 bytes, big-endian)
193    if let Some(valid_before) = message.get("validBefore") {
194        if let Some(valid_before_str) = valid_before.as_str() {
195            let valid_before_hex = valid_before_str.trim_start_matches("0x");
196            let valid_before_bytes = hex::decode(valid_before_hex)
197                .map_err(|_| X402Error::invalid_authorization("Invalid validBefore format"))?;
198            let mut padded = [0u8; 32];
199            let start = 32 - valid_before_bytes.len();
200            padded[start..].copy_from_slice(&valid_before_bytes);
201            encoded.extend_from_slice(&padded);
202        }
203    }
204
205    // Encode 'nonce' (32 bytes)
206    if let Some(nonce) = message.get("nonce") {
207        if let Some(nonce_str) = nonce.as_str() {
208            let nonce_hex = nonce_str.trim_start_matches("0x");
209            let nonce_bytes = hex::decode(nonce_hex)
210                .map_err(|_| X402Error::invalid_authorization("Invalid nonce format"))?;
211            if nonce_bytes.len() != 32 {
212                return Err(X402Error::invalid_authorization("Nonce must be 32 bytes"));
213            }
214            encoded.extend_from_slice(&nonce_bytes);
215        }
216    }
217
218    Ok(encoded)
219}
220
221/// Keccak-256 hash function
222pub fn keccak256(data: &[u8]) -> [u8; 32] {
223    use sha3::{Digest, Keccak256};
224    Keccak256::digest(data).into()
225}
226
227/// SHA3-256 hash function
228///
229/// This function is available for applications that need SHA3-256 hashing
230/// in addition to Keccak-256. While EIP-712 primarily uses Keccak-256,
231/// SHA3-256 may be needed for other cryptographic operations.
232pub fn sha3_256(data: &[u8]) -> [u8; 32] {
233    use sha3::{Digest, Sha3_256};
234    Sha3_256::digest(data).into()
235}