ecash_client/
wallet.rs

1use crate::api::{ApiClient, RedeemRequest, WithdrawRequest};
2use crate::error::{ClientError, Result};
3use crate::storage::{StoredToken, WalletStorage};
4use chrono::{DateTime, Utc};
5use ecash_core::{Token, Wallet as CoreWallet};
6use rsa::RsaPublicKey;
7
8pub struct Wallet {
9    api: ApiClient,
10    storage: WalletStorage,
11    core_wallet: Option<CoreWallet>,
12    institution_id: String,
13}
14
15impl Wallet {
16    pub fn new(server_url: String, db_path: String) -> Result<Self> {
17        let api = ApiClient::new(server_url);
18        let storage = WalletStorage::new(&db_path)?;
19        
20        Ok(Self {
21            api,
22            storage,
23            core_wallet: None,
24            institution_id: String::new(),
25        })
26    }
27
28    pub async fn initialize(&mut self) -> Result<()> {
29        let key_response = self.api.get_public_key().await?;
30        
31        // Parse decimal strings to BigUint
32        let n = rsa::BigUint::parse_bytes(key_response.public_key_n.as_bytes(), 10)
33            .ok_or_else(|| ClientError::InvalidResponse("Invalid public key N".to_string()))?;
34        
35        let e = rsa::BigUint::parse_bytes(key_response.public_key_e.as_bytes(), 10)
36            .ok_or_else(|| ClientError::InvalidResponse("Invalid public key E".to_string()))?;
37        
38        let public_key = RsaPublicKey::new(n, e)
39            .map_err(|e| ClientError::InvalidResponse(format!("Invalid public key: {}", e)))?;
40        
41        self.core_wallet = Some(CoreWallet::new(
42            public_key,
43            key_response.institution_id.clone(),
44            "USD".to_string(),
45        ));
46        
47        self.institution_id = key_response.institution_id;
48        
49        Ok(())
50    }
51
52    pub async fn withdraw(&self, amount: u64, denomination: u64) -> Result<Vec<Token>> {
53        let core_wallet = self.core_wallet.as_ref()
54            .ok_or_else(|| ClientError::InvalidResponse("Wallet not initialized".to_string()))?;
55        
56        let tokens_to_prepare = core_wallet.prepare_withdrawal(amount, denomination)
57            .map_err(ClientError::Core)?;
58        
59        let (blinded_tokens, metadata): (Vec<_>, Vec<_>) = tokens_to_prepare.into_iter().unzip();
60        
61        let request = WithdrawRequest {
62            amount,
63            denomination,
64            blinded_tokens: blinded_tokens.clone(),
65        };
66        
67        let response = self.api.withdraw(request).await?;
68        
69        let expires_at = DateTime::parse_from_rfc3339(&response.expires_at)
70            .map_err(|e| ClientError::InvalidResponse(format!("Invalid expires_at: {}", e)))?
71            .with_timezone(&Utc);
72        
73        let tokens = core_wallet.finalize_withdrawal(
74            response.blind_signatures,
75            metadata,
76            expires_at,
77        ).map_err(ClientError::Core)?;
78        
79        for token in &tokens {
80            self.storage.store_token(token.clone())?;
81        }
82        
83        self.storage.log_transaction(
84            "withdraw",
85            amount,
86            tokens.len(),
87            Some(response.transaction_id),
88        )?;
89        
90        Ok(tokens)
91    }
92
93    pub async fn spend(&self, amount: u64) -> Result<String> {
94        let available = self.storage.get_available_tokens()?;
95        
96        if available.is_empty() {
97            return Err(ClientError::NoTokensAvailable);
98        }
99        
100        let mut selected_tokens = Vec::new();
101        let mut total = 0u64;
102        let mut token_ids = Vec::new();
103        
104        for stored_token in available {
105            if total >= amount {
106                break;
107            }
108            total += stored_token.token.denomination;
109            token_ids.push(stored_token.id);
110            selected_tokens.push(stored_token.token);
111        }
112        
113        if total < amount {
114            return Err(ClientError::InsufficientBalance { 
115                required: amount, 
116                available: total 
117            });
118        }
119        
120        let request = RedeemRequest {
121            tokens: selected_tokens,
122            merchant_id: Some("self".to_string()),
123        };
124        
125        let response = self.api.redeem(request).await?;
126        
127        self.storage.mark_tokens_spent(&token_ids)?;
128        
129        self.storage.log_transaction(
130            "spend",
131            amount,
132            token_ids.len(),
133            Some(response.transaction_id.clone()),
134        )?;
135        
136        Ok(response.transaction_id)
137    }
138
139    pub fn get_balance(&self) -> Result<u64> {
140        self.storage.get_balance()
141    }
142
143    pub fn get_available_tokens(&self) -> Result<Vec<StoredToken>> {
144        self.storage.get_available_tokens()
145    }
146
147    pub async fn health_check(&self) -> Result<bool> {
148        self.api.health_check().await
149    }
150}