1use crate::balance::{Balance, BalanceTracker};
8use crate::error::{Result, WalletError};
9use crate::history::{TransactionHistory, TxRecord, TxStatus};
10use crate::nonce::NonceManager;
11use async_trait::async_trait;
12use std::sync::Arc;
13use tenzro_types::primitives::{Address, Hash, Signature};
14use tenzro_types::{AssetId, Transaction};
15use tracing::{debug, info, warn};
16
17#[async_trait]
23pub trait ChainStateProvider: Send + Sync {
24 async fn get_on_chain_balance(
26 &self,
27 address: &Address,
28 asset_id: &AssetId,
29 ) -> Result<u128>;
30
31 async fn get_on_chain_balances(
33 &self,
34 address: &Address,
35 ) -> Result<Vec<(AssetId, u128)>>;
36
37 async fn get_on_chain_nonce(&self, address: &Address) -> Result<u64>;
39
40 async fn get_transaction_status(&self, tx_hash: &Hash) -> Result<TxStatus>;
42
43 async fn get_block_height(&self) -> Result<u64>;
45
46 async fn submit_signed_transaction(
61 &self,
62 tx: &Transaction,
63 classical_sig: &Signature,
64 pq_sig: &[u8],
65 ) -> Result<Hash>;
66}
67
68pub struct WalletStateSync {
77 balances: Arc<BalanceTracker>,
79 nonces: Arc<NonceManager>,
81 history: Arc<TransactionHistory>,
83 chain_provider: Option<Arc<dyn ChainStateProvider>>,
85}
86
87impl WalletStateSync {
88 pub fn new(
90 balances: Arc<BalanceTracker>,
91 nonces: Arc<NonceManager>,
92 history: Arc<TransactionHistory>,
93 ) -> Self {
94 Self {
95 balances,
96 nonces,
97 history,
98 chain_provider: None,
99 }
100 }
101
102 pub fn with_chain_provider(mut self, provider: Arc<dyn ChainStateProvider>) -> Self {
104 self.chain_provider = Some(provider);
105 self
106 }
107
108 pub fn is_connected(&self) -> bool {
110 self.chain_provider.is_some()
111 }
112
113 pub async fn sync_address(&self, address: &Address, assets: &[AssetId]) -> Result<()> {
118 let provider = self.chain_provider.as_ref().ok_or_else(|| {
119 WalletError::Other("No chain state provider connected".to_string())
120 })?;
121
122 self.sync_balances(address, assets, provider.as_ref()).await?;
124
125 self.sync_nonce(address, provider.as_ref()).await?;
127
128 self.sync_pending_transactions(address, provider.as_ref()).await?;
130
131 info!("Completed full sync for address {}", address);
132 Ok(())
133 }
134
135 async fn sync_balances(
137 &self,
138 address: &Address,
139 assets: &[AssetId],
140 provider: &dyn ChainStateProvider,
141 ) -> Result<()> {
142 for asset_id in assets {
143 match provider.get_on_chain_balance(address, asset_id).await {
144 Ok(on_chain_balance) => {
145 let current = self.balances.get_balance(address, asset_id);
146
147 let synced = Balance {
149 available: on_chain_balance,
150 locked: current.locked,
151 pending_in: current.pending_in,
152 pending_out: current.pending_out,
153 };
154 self.balances.set_balance(address, asset_id, synced);
155
156 debug!(
157 "Synced balance for {} {}: {} → {}",
158 address,
159 asset_id.as_str(),
160 current.available,
161 on_chain_balance
162 );
163 }
164 Err(e) => {
165 warn!(
166 "Failed to sync balance for {} {}: {}",
167 address,
168 asset_id.as_str(),
169 e
170 );
171 }
172 }
173 }
174 Ok(())
175 }
176
177 async fn sync_nonce(
179 &self,
180 address: &Address,
181 provider: &dyn ChainStateProvider,
182 ) -> Result<()> {
183 match provider.get_on_chain_nonce(address).await {
184 Ok(on_chain_nonce) => {
185 self.nonces.sync_from_chain(address, on_chain_nonce);
186 debug!(
187 "Synced nonce for {}: on-chain={}",
188 address, on_chain_nonce
189 );
190 }
191 Err(e) => {
192 warn!("Failed to sync nonce for {}: {}", address, e);
193 }
194 }
195 Ok(())
196 }
197
198 async fn sync_pending_transactions(
200 &self,
201 address: &Address,
202 provider: &dyn ChainStateProvider,
203 ) -> Result<()> {
204 let pending_records: Vec<TxRecord> = self
205 .history
206 .get_history(address)
207 .into_iter()
208 .filter(|r| r.is_pending())
209 .collect();
210
211 for record in pending_records {
212 match provider.get_transaction_status(&record.tx_hash).await {
213 Ok(new_status) => {
214 if new_status != record.status {
215 if let Err(e) = self.history.update_status(&record.tx_hash, new_status) {
216 warn!("Failed to update tx status for {}: {}", record.tx_hash, e);
217 } else {
218 debug!(
219 "Updated tx {} status: {} → {}",
220 record.tx_hash, record.status, new_status
221 );
222 }
223 }
224 }
225 Err(e) => {
226 warn!(
227 "Failed to check status for tx {}: {}",
228 record.tx_hash, e
229 );
230 }
231 }
232 }
233
234 Ok(())
235 }
236
237 pub async fn get_balance(&self, address: &Address, asset_id: &AssetId) -> Result<Balance> {
239 if let Some(provider) = &self.chain_provider {
241 match provider.get_on_chain_balance(address, asset_id).await {
242 Ok(on_chain) => {
243 let current = self.balances.get_balance(address, asset_id);
244 return Ok(Balance {
245 available: on_chain,
246 locked: current.locked,
247 pending_in: current.pending_in,
248 pending_out: current.pending_out,
249 });
250 }
251 Err(e) => {
252 debug!(
253 "Falling back to cached balance for {} {}: {}",
254 address,
255 asset_id.as_str(),
256 e
257 );
258 }
259 }
260 }
261
262 Ok(self.balances.get_balance(address, asset_id))
264 }
265
266 pub async fn submit_and_track(
273 &self,
274 address: &Address,
275 tx: &Transaction,
276 classical_sig: &Signature,
277 pq_sig: &[u8],
278 record: TxRecord,
279 ) -> Result<Hash> {
280 let provider = self.chain_provider.as_ref().ok_or_else(|| {
281 WalletError::Other("No chain state provider connected".to_string())
282 })?;
283
284 let tx_hash = provider
286 .submit_signed_transaction(tx, classical_sig, pq_sig)
287 .await?;
288
289 let mut tracked = record;
291 tracked.tx_hash = tx_hash;
292 tracked.mark_pending();
293 self.history.record(address, tracked);
294
295 info!("Submitted and tracking transaction {}", tx_hash);
296 Ok(tx_hash)
297 }
298}
299
300pub struct LocalStateProvider {
305 balances: Arc<BalanceTracker>,
306 nonces: Arc<NonceManager>,
307}
308
309impl LocalStateProvider {
310 pub fn new(balances: Arc<BalanceTracker>, nonces: Arc<NonceManager>) -> Self {
312 Self { balances, nonces }
313 }
314}
315
316#[async_trait]
317impl ChainStateProvider for LocalStateProvider {
318 async fn get_on_chain_balance(
319 &self,
320 address: &Address,
321 asset_id: &AssetId,
322 ) -> Result<u128> {
323 Ok(self.balances.get_balance(address, asset_id).available)
324 }
325
326 async fn get_on_chain_balances(
327 &self,
328 address: &Address,
329 ) -> Result<Vec<(AssetId, u128)>> {
330 Ok(self
331 .balances
332 .get_all_balances(address)
333 .into_iter()
334 .map(|(id, bal)| (id, bal.available))
335 .collect())
336 }
337
338 async fn get_on_chain_nonce(&self, address: &Address) -> Result<u64> {
339 Ok(self.nonces.confirmed_nonce(address).0)
340 }
341
342 async fn get_transaction_status(&self, _tx_hash: &Hash) -> Result<TxStatus> {
343 Ok(TxStatus::Pending)
345 }
346
347 async fn get_block_height(&self) -> Result<u64> {
348 Ok(0)
349 }
350
351 async fn submit_signed_transaction(
352 &self,
353 tx: &Transaction,
354 _classical_sig: &Signature,
355 _pq_sig: &[u8],
356 ) -> Result<Hash> {
357 Ok(tx.hash())
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368
369 #[tokio::test]
370 async fn test_local_state_provider() {
371 let balances = Arc::new(BalanceTracker::new());
372 let nonces = Arc::new(NonceManager::new());
373
374 let addr = Address::new([1u8; 32]);
375 let asset = AssetId::tnzo();
376
377 balances.add_balance(&addr, &asset, 1000);
379 nonces.next_nonce(&addr); nonces.confirm_nonce(&addr, 0);
381
382 let provider = LocalStateProvider::new(balances.clone(), nonces.clone());
383
384 let balance = provider.get_on_chain_balance(&addr, &asset).await.unwrap();
385 assert_eq!(balance, 1000);
386
387 let nonce = provider.get_on_chain_nonce(&addr).await.unwrap();
388 assert_eq!(nonce, 1);
389 }
390
391 #[tokio::test]
392 async fn test_wallet_state_sync() {
393 let balances = Arc::new(BalanceTracker::new());
394 let nonces = Arc::new(NonceManager::new());
395 let history = Arc::new(TransactionHistory::new());
396
397 let addr = Address::new([1u8; 32]);
398 let asset = AssetId::tnzo();
399
400 balances.add_balance(&addr, &asset, 1000);
402
403 let provider = Arc::new(LocalStateProvider::new(
404 balances.clone(),
405 nonces.clone(),
406 ));
407
408 let sync = WalletStateSync::new(
409 balances.clone(),
410 nonces.clone(),
411 history.clone(),
412 )
413 .with_chain_provider(provider);
414
415 assert!(sync.is_connected());
416
417 let balance = sync.get_balance(&addr, &asset).await.unwrap();
418 assert_eq!(balance.available, 1000);
419 }
420
421 #[tokio::test]
422 async fn test_sync_address() {
423 let balances = Arc::new(BalanceTracker::new());
424 let nonces = Arc::new(NonceManager::new());
425 let history = Arc::new(TransactionHistory::new());
426
427 let addr = Address::new([1u8; 32]);
428 let asset = AssetId::tnzo();
429
430 balances.add_balance(&addr, &asset, 5000);
431
432 let provider = Arc::new(LocalStateProvider::new(
433 balances.clone(),
434 nonces.clone(),
435 ));
436
437 let sync = WalletStateSync::new(
438 balances.clone(),
439 nonces.clone(),
440 history.clone(),
441 )
442 .with_chain_provider(provider);
443
444 sync.sync_address(&addr, std::slice::from_ref(&asset)).await.unwrap();
445
446 let balance = balances.get_balance(&addr, &asset);
447 assert_eq!(balance.available, 5000);
448 }
449
450 #[tokio::test]
451 async fn test_offline_fallback() {
452 let balances = Arc::new(BalanceTracker::new());
453 let nonces = Arc::new(NonceManager::new());
454 let history = Arc::new(TransactionHistory::new());
455
456 let addr = Address::new([1u8; 32]);
457 let asset = AssetId::tnzo();
458
459 balances.add_balance(&addr, &asset, 2000);
460
461 let sync = WalletStateSync::new(
463 balances.clone(),
464 nonces.clone(),
465 history.clone(),
466 );
467
468 assert!(!sync.is_connected());
469
470 let balance = sync.get_balance(&addr, &asset).await.unwrap();
472 assert_eq!(balance.available, 2000);
473 }
474}