rust_x402/
wallet.rs

1//! Real wallet integration for x402 payments
2//!
3//! This module provides real wallet implementations for creating and signing
4//! x402 payment payloads with actual private keys and EIP-712 signatures.
5
6use crate::{
7    crypto::{
8        eip712::{create_transfer_with_authorization_hash, Domain},
9        signature::{generate_nonce, sign_message_hash, verify_payment_payload},
10    },
11    types::{ExactEvmPayload, ExactEvmPayloadAuthorization, PaymentPayload, PaymentRequirements},
12    Result, X402Error,
13};
14use ethereum_types::{Address, U256};
15use std::str::FromStr;
16
17/// Wallet implementation for x402 payments
18#[derive(Debug)]
19pub struct Wallet {
20    /// Private key for signing (in production, this should come from secure storage)
21    private_key: String,
22    /// Network configuration
23    network: String,
24}
25
26impl Wallet {
27    /// Create a new wallet instance
28    ///
29    /// # Security Note
30    /// In production, the private key should be loaded from:
31    /// - Hardware wallets (Ledger, Trezor)
32    /// - Encrypted key stores
33    /// - Secure environment variables
34    /// - Key management services (AWS KMS, Azure Key Vault)
35    pub fn new(private_key: String, network: String) -> Self {
36        Self {
37            private_key,
38            network,
39        }
40    }
41
42    /// Create a payment payload with real EIP-712 signature
43    ///
44    /// This is the production-ready implementation that:
45    /// 1. Generates cryptographically secure random nonce
46    /// 2. Creates proper EIP-712 message hash
47    /// 3. Signs with the user's private key
48    /// 4. Verifies the signature before returning
49    pub fn create_signed_payment_payload(
50        &self,
51        requirements: &PaymentRequirements,
52        from_address: &str,
53    ) -> Result<PaymentPayload> {
54        // Step 1: Generate cryptographically secure nonce
55        let nonce = generate_nonce();
56
57        // Step 2: Set appropriate timestamps
58        let now = chrono::Utc::now().timestamp();
59        let valid_after = (now - 60).to_string(); // Allow 1 minute leeway
60        let valid_before = (now + 300).to_string(); // 5 minutes validity window
61
62        // Step 3: Create the authorization
63        let authorization = ExactEvmPayloadAuthorization::new(
64            from_address,
65            &requirements.pay_to,
66            &requirements.max_amount_required,
67            valid_after,
68            valid_before,
69            format!("{:?}", nonce),
70        );
71
72        // Step 4: Create the EIP-712 message hash
73        let network_config = self.get_network_config()?;
74        let domain = Domain {
75            name: "USD Coin".to_string(),
76            version: "2".to_string(),
77            chain_id: network_config.chain_id,
78            verifying_contract: network_config.usdc_contract,
79        };
80
81        let message_hash = create_transfer_with_authorization_hash(
82            &domain,
83            Address::from_str(from_address)
84                .map_err(|_| X402Error::invalid_authorization("Invalid from address format"))?,
85            Address::from_str(&requirements.pay_to)
86                .map_err(|_| X402Error::invalid_authorization("Invalid pay_to address format"))?,
87            U256::from_str_radix(&requirements.max_amount_required, 10)
88                .map_err(|_| X402Error::invalid_authorization("Invalid amount format"))?,
89            U256::from_str_radix(&authorization.valid_after, 10)
90                .map_err(|_| X402Error::invalid_authorization("Invalid valid_after format"))?,
91            U256::from_str_radix(&authorization.valid_before, 10)
92                .map_err(|_| X402Error::invalid_authorization("Invalid valid_before format"))?,
93            nonce,
94        )?;
95
96        // Step 5: Sign the message hash with the private key
97        let signature = sign_message_hash(message_hash, &self.private_key)?;
98
99        // Step 6: Create the payload
100        let payload = ExactEvmPayload {
101            signature,
102            authorization,
103        };
104
105        let payment_payload =
106            PaymentPayload::new(&requirements.scheme, &requirements.network, payload);
107
108        // Step 7: Verify the signature (production best practice)
109        let is_valid =
110            verify_payment_payload(&payment_payload.payload, from_address, &self.network)?;
111
112        if !is_valid {
113            return Err(X402Error::invalid_signature(
114                "Generated signature verification failed",
115            ));
116        }
117
118        Ok(payment_payload)
119    }
120
121    /// Get network configuration for the current network
122    pub fn get_network_config(&self) -> Result<WalletNetworkConfig> {
123        match self.network.as_str() {
124            "base-sepolia" => Ok(WalletNetworkConfig {
125                chain_id: 84532,
126                usdc_contract: Address::from_str("0x036CbD53842c5426634e7929541eC2318f3dCF7e")
127                    .map_err(|_| X402Error::invalid_network("Invalid USDC contract address"))?,
128            }),
129            "base" => Ok(WalletNetworkConfig {
130                chain_id: 8453,
131                usdc_contract: Address::from_str("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913")
132                    .map_err(|_| X402Error::invalid_network("Invalid USDC contract address"))?,
133            }),
134            "avalanche-fuji" => Ok(WalletNetworkConfig {
135                chain_id: 43113,
136                usdc_contract: Address::from_str("0x5425890298aed601595a70AB815c96711a31Bc65")
137                    .map_err(|_| X402Error::invalid_network("Invalid USDC contract address"))?,
138            }),
139            "avalanche" => Ok(WalletNetworkConfig {
140                chain_id: 43114,
141                usdc_contract: Address::from_str("0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E")
142                    .map_err(|_| X402Error::invalid_network("Invalid USDC contract address"))?,
143            }),
144            _ => Err(X402Error::invalid_network(format!(
145                "Unsupported network: {}",
146                self.network
147            ))),
148        }
149    }
150
151    /// Get the network name
152    pub fn network(&self) -> &str {
153        &self.network
154    }
155}
156
157/// Wallet network configuration for different blockchains
158#[derive(Debug, Clone)]
159pub struct WalletNetworkConfig {
160    pub chain_id: u64,
161    pub usdc_contract: Address,
162}
163
164/// Wallet factory for creating wallets from different sources
165pub struct WalletFactory;
166
167impl WalletFactory {
168    /// Create wallet from private key string
169    pub fn from_private_key(private_key: &str, network: &str) -> Result<Wallet> {
170        // Validate private key format
171        if !private_key.starts_with("0x") || private_key.len() != 66 {
172            return Err(X402Error::invalid_authorization(
173                "Invalid private key format. Must be 64 hex characters with 0x prefix",
174            ));
175        }
176
177        // Validate hex format
178        hex::decode(&private_key[2..])
179            .map_err(|_| X402Error::invalid_authorization("Invalid hex in private key"))?;
180
181        Ok(Wallet::new(private_key.to_string(), network.to_string()))
182    }
183
184    /// Create wallet from environment variable
185    pub fn from_env(private_key_env: &str, network: &str) -> Result<Wallet> {
186        let private_key = std::env::var(private_key_env).map_err(|_| {
187            X402Error::config(format!(
188                "Environment variable {} not found",
189                private_key_env
190            ))
191        })?;
192
193        Self::from_private_key(&private_key, network)
194    }
195
196    /// Create wallet with network from environment
197    pub fn from_env_with_network(private_key_env: &str, network_env: &str) -> Result<Wallet> {
198        let private_key = std::env::var(private_key_env).map_err(|_| {
199            X402Error::config(format!(
200                "Environment variable {} not found",
201                private_key_env
202            ))
203        })?;
204
205        let network = std::env::var(network_env).map_err(|_| {
206            X402Error::config(format!("Environment variable {} not found", network_env))
207        })?;
208
209        Self::from_private_key(&private_key, &network)
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    #[test]
218    fn test_wallet_creation() {
219        let wallet = Wallet::new(
220            "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".to_string(),
221            "base-sepolia".to_string(),
222        );
223        assert_eq!(wallet.network(), "base-sepolia");
224    }
225
226    #[test]
227    fn test_wallet_factory_valid_key() {
228        let wallet = WalletFactory::from_private_key(
229            "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
230            "base-sepolia",
231        );
232        assert!(wallet.is_ok());
233    }
234
235    #[test]
236    fn test_wallet_factory_invalid_key() {
237        let wallet = WalletFactory::from_private_key("invalid", "base-sepolia");
238        assert!(wallet.is_err(), "Invalid private key should fail");
239
240        // Verify the specific error type
241        let error = wallet.unwrap_err();
242        match error {
243            X402Error::InvalidAuthorization { message: _ } => {
244                // This is the expected error type
245            }
246            _ => panic!("Expected InvalidAuthorization error, got: {:?}", error),
247        }
248    }
249
250    #[test]
251    fn test_wallet_factory_edge_cases() {
252        // Test empty string
253        let wallet = WalletFactory::from_private_key("", "base-sepolia");
254        assert!(wallet.is_err(), "Empty private key should fail");
255
256        // Test too short key
257        let wallet = WalletFactory::from_private_key("0x123", "base-sepolia");
258        assert!(wallet.is_err(), "Too short private key should fail");
259
260        // Test too long key
261        let wallet = WalletFactory::from_private_key("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "base-sepolia");
262        assert!(wallet.is_err(), "Too long private key should fail");
263
264        // Test invalid hex characters
265        let wallet = WalletFactory::from_private_key(
266            "0xgggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg",
267            "base-sepolia",
268        );
269        assert!(wallet.is_err(), "Invalid hex characters should fail");
270
271        // Test missing 0x prefix
272        let wallet = WalletFactory::from_private_key(
273            "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
274            "base-sepolia",
275        );
276        assert!(wallet.is_err(), "Missing 0x prefix should fail");
277    }
278
279    #[test]
280    fn test_network_config() {
281        let wallet = Wallet::new(
282            "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".to_string(),
283            "base-sepolia".to_string(),
284        );
285        let config = wallet.get_network_config().unwrap();
286        assert_eq!(config.chain_id, 84532);
287    }
288}