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 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}