use crate::{
crypto::{
eip712::{create_transfer_with_authorization_hash, Domain},
signature::{generate_nonce, sign_message_hash, verify_payment_payload},
},
types::{ExactEvmPayload, ExactEvmPayloadAuthorization, PaymentPayload, PaymentRequirements},
Result, X402Error,
};
use ethereum_types::{Address, U256};
use std::str::FromStr;
#[derive(Debug)]
pub struct Wallet {
private_key: String,
network: String,
}
impl Wallet {
pub fn new(private_key: String, network: String) -> Self {
Self {
private_key,
network,
}
}
pub fn create_signed_payment_payload(
&self,
requirements: &PaymentRequirements,
from_address: &str,
) -> Result<PaymentPayload> {
let nonce = generate_nonce();
let now = chrono::Utc::now().timestamp();
let valid_after = (now - 60).to_string(); let valid_before = (now + 300).to_string();
let authorization = ExactEvmPayloadAuthorization::new(
from_address,
&requirements.pay_to,
&requirements.max_amount_required,
valid_after,
valid_before,
format!("{:?}", nonce),
);
let network_config = self.get_network_config()?;
let domain = Domain {
name: "USD Coin".to_string(),
version: "2".to_string(),
chain_id: network_config.chain_id,
verifying_contract: network_config.usdc_contract,
};
let message_hash = create_transfer_with_authorization_hash(
&domain,
Address::from_str(from_address)
.map_err(|_| X402Error::invalid_authorization("Invalid from address format"))?,
Address::from_str(&requirements.pay_to)
.map_err(|_| X402Error::invalid_authorization("Invalid pay_to address format"))?,
U256::from_str_radix(&requirements.max_amount_required, 10)
.map_err(|_| X402Error::invalid_authorization("Invalid amount format"))?,
U256::from_str_radix(&authorization.valid_after, 10)
.map_err(|_| X402Error::invalid_authorization("Invalid valid_after format"))?,
U256::from_str_radix(&authorization.valid_before, 10)
.map_err(|_| X402Error::invalid_authorization("Invalid valid_before format"))?,
nonce,
)?;
let signature = sign_message_hash(message_hash, &self.private_key)?;
let payload = ExactEvmPayload {
signature,
authorization,
};
let payment_payload =
PaymentPayload::new(&requirements.scheme, &requirements.network, payload);
let is_valid =
verify_payment_payload(&payment_payload.payload, from_address, &self.network)?;
if !is_valid {
return Err(X402Error::invalid_signature(
"Generated signature verification failed",
));
}
Ok(payment_payload)
}
pub fn get_network_config(&self) -> Result<WalletNetworkConfig> {
match self.network.as_str() {
"base-sepolia" => Ok(WalletNetworkConfig {
chain_id: 84532,
usdc_contract: Address::from_str("0x036CbD53842c5426634e7929541eC2318f3dCF7e")
.map_err(|_| X402Error::invalid_network("Invalid USDC contract address"))?,
}),
"base" => Ok(WalletNetworkConfig {
chain_id: 8453,
usdc_contract: Address::from_str("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913")
.map_err(|_| X402Error::invalid_network("Invalid USDC contract address"))?,
}),
"avalanche-fuji" => Ok(WalletNetworkConfig {
chain_id: 43113,
usdc_contract: Address::from_str("0x5425890298aed601595a70AB815c96711a31Bc65")
.map_err(|_| X402Error::invalid_network("Invalid USDC contract address"))?,
}),
"avalanche" => Ok(WalletNetworkConfig {
chain_id: 43114,
usdc_contract: Address::from_str("0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E")
.map_err(|_| X402Error::invalid_network("Invalid USDC contract address"))?,
}),
_ => Err(X402Error::invalid_network(format!(
"Unsupported network: {}",
self.network
))),
}
}
pub fn network(&self) -> &str {
&self.network
}
}
#[derive(Debug, Clone)]
pub struct WalletNetworkConfig {
pub chain_id: u64,
pub usdc_contract: Address,
}
pub struct WalletFactory;
impl WalletFactory {
pub fn from_private_key(private_key: &str, network: &str) -> Result<Wallet> {
if !private_key.starts_with("0x") || private_key.len() != 66 {
return Err(X402Error::invalid_authorization(
"Invalid private key format. Must be 64 hex characters with 0x prefix",
));
}
hex::decode(&private_key[2..])
.map_err(|_| X402Error::invalid_authorization("Invalid hex in private key"))?;
Ok(Wallet::new(private_key.to_string(), network.to_string()))
}
pub fn from_env(private_key_env: &str, network: &str) -> Result<Wallet> {
let private_key = std::env::var(private_key_env).map_err(|_| {
X402Error::config(format!(
"Environment variable {} not found",
private_key_env
))
})?;
Self::from_private_key(&private_key, network)
}
pub fn from_env_with_network(private_key_env: &str, network_env: &str) -> Result<Wallet> {
let private_key = std::env::var(private_key_env).map_err(|_| {
X402Error::config(format!(
"Environment variable {} not found",
private_key_env
))
})?;
let network = std::env::var(network_env).map_err(|_| {
X402Error::config(format!("Environment variable {} not found", network_env))
})?;
Self::from_private_key(&private_key, &network)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_wallet_creation() {
let wallet = Wallet::new(
"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".to_string(),
"base-sepolia".to_string(),
);
assert_eq!(wallet.network(), "base-sepolia");
}
#[test]
fn test_wallet_factory_valid_key() {
let wallet = WalletFactory::from_private_key(
"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"base-sepolia",
);
assert!(wallet.is_ok());
}
#[test]
fn test_wallet_factory_invalid_key() {
let wallet = WalletFactory::from_private_key("invalid", "base-sepolia");
assert!(wallet.is_err(), "Invalid private key should fail");
let error = wallet.unwrap_err();
match error {
X402Error::InvalidAuthorization { message: _ } => {
}
_ => panic!("Expected InvalidAuthorization error, got: {:?}", error),
}
}
#[test]
fn test_wallet_factory_edge_cases() {
let wallet = WalletFactory::from_private_key("", "base-sepolia");
assert!(wallet.is_err(), "Empty private key should fail");
let wallet = WalletFactory::from_private_key("0x123", "base-sepolia");
assert!(wallet.is_err(), "Too short private key should fail");
let wallet = WalletFactory::from_private_key("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "base-sepolia");
assert!(wallet.is_err(), "Too long private key should fail");
let wallet = WalletFactory::from_private_key(
"0xgggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg",
"base-sepolia",
);
assert!(wallet.is_err(), "Invalid hex characters should fail");
let wallet = WalletFactory::from_private_key(
"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"base-sepolia",
);
assert!(wallet.is_err(), "Missing 0x prefix should fail");
}
#[test]
fn test_network_config() {
let wallet = Wallet::new(
"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".to_string(),
"base-sepolia".to_string(),
);
let config = wallet.get_network_config().unwrap();
assert_eq!(config.chain_id, 84532);
}
}