1use 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#[derive(Debug)]
19pub struct Wallet {
20 private_key: String,
22 network: String,
24}
25
26impl Wallet {
27 pub fn new(private_key: String, network: String) -> Self {
36 Self {
37 private_key,
38 network,
39 }
40 }
41
42 pub fn create_signed_payment_payload(
50 &self,
51 requirements: &PaymentRequirements,
52 from_address: &str,
53 ) -> Result<PaymentPayload> {
54 let nonce = generate_nonce();
56
57 let now = chrono::Utc::now().timestamp();
59 let valid_after = (now - 60).to_string(); let valid_before = (now + 300).to_string(); 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 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 let signature = sign_message_hash(message_hash, &self.private_key)?;
98
99 let payload = ExactEvmPayload {
101 signature,
102 authorization,
103 };
104
105 let payment_payload =
106 PaymentPayload::new(&requirements.scheme, &requirements.network, payload);
107
108 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 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 pub fn network(&self) -> &str {
153 &self.network
154 }
155}
156
157#[derive(Debug, Clone)]
159pub struct WalletNetworkConfig {
160 pub chain_id: u64,
161 pub usdc_contract: Address,
162}
163
164pub struct WalletFactory;
166
167impl WalletFactory {
168 pub fn from_private_key(private_key: &str, network: &str) -> Result<Wallet> {
170 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 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 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 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 let error = wallet.unwrap_err();
242 match error {
243 X402Error::InvalidAuthorization { message: _ } => {
244 }
246 _ => panic!("Expected InvalidAuthorization error, got: {:?}", error),
247 }
248 }
249
250 #[test]
251 fn test_wallet_factory_edge_cases() {
252 let wallet = WalletFactory::from_private_key("", "base-sepolia");
254 assert!(wallet.is_err(), "Empty private key should fail");
255
256 let wallet = WalletFactory::from_private_key("0x123", "base-sepolia");
258 assert!(wallet.is_err(), "Too short private key should fail");
259
260 let wallet = WalletFactory::from_private_key("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "base-sepolia");
262 assert!(wallet.is_err(), "Too long private key should fail");
263
264 let wallet = WalletFactory::from_private_key(
266 "0xgggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg",
267 "base-sepolia",
268 );
269 assert!(wallet.is_err(), "Invalid hex characters should fail");
270
271 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}