Skip to main content

rabia_banking_example/
lib.rs

1//! # Banking SMR Example
2//!
3//! A banking ledger implementation that demonstrates how to create a complex State Machine
4//! Replication (SMR) application using the Rabia consensus protocol.
5//!
6//! This example shows a more sophisticated SMR implementation with:
7//! - Account management
8//! - Transfer operations with validation
9//! - Transaction history
10//! - Balance tracking
11//! - Error handling for insufficient funds
12//!
13//! ## Example Usage
14//!
15//! ```rust
16//! use rabia_banking_example::{BankingSMR, BankingCommand};
17//! use rabia_core::smr::StateMachine;
18//!
19//! #[tokio::main]
20//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
21//!     let mut bank = BankingSMR::new();
22//!     
23//!     // Create accounts
24//!     let create_cmd = BankingCommand::CreateAccount {
25//!         account_id: "alice".to_string(),
26//!         initial_balance: 1000,
27//!     };
28//!     let response = bank.apply_command(create_cmd).await;
29//!     println!("Account created: {:?}", response);
30//!     
31//!     Ok(())
32//! }
33//! ```
34
35use async_trait::async_trait;
36use rabia_core::smr::StateMachine;
37use serde::{Deserialize, Serialize};
38use std::collections::HashMap;
39
40/// Account information
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
42pub struct Account {
43    /// Account identifier
44    pub account_id: String,
45    /// Current balance in cents (to avoid floating point precision issues)
46    pub balance: i64,
47    /// Account creation timestamp
48    pub created_at: u64,
49    /// Last transaction timestamp
50    pub last_transaction_at: u64,
51    /// Total number of transactions
52    pub transaction_count: u64,
53}
54
55impl Account {
56    pub fn new(account_id: String, initial_balance: i64) -> Self {
57        let now = std::time::SystemTime::now()
58            .duration_since(std::time::UNIX_EPOCH)
59            .unwrap_or_default()
60            .as_millis() as u64;
61
62        Self {
63            account_id,
64            balance: initial_balance,
65            created_at: now,
66            last_transaction_at: now,
67            transaction_count: 0,
68        }
69    }
70
71    pub fn update_balance(&mut self, new_balance: i64) {
72        self.balance = new_balance;
73        self.last_transaction_at = std::time::SystemTime::now()
74            .duration_since(std::time::UNIX_EPOCH)
75            .unwrap_or_default()
76            .as_millis() as u64;
77        self.transaction_count += 1;
78    }
79}
80
81/// Transaction record
82#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
83pub struct Transaction {
84    /// Unique transaction ID
85    pub transaction_id: String,
86    /// Source account (None for deposits)
87    pub from_account: Option<String>,
88    /// Destination account (None for withdrawals)
89    pub to_account: Option<String>,
90    /// Amount in cents
91    pub amount: i64,
92    /// Transaction timestamp
93    pub timestamp: u64,
94    /// Transaction type
95    pub transaction_type: TransactionType,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
99pub enum TransactionType {
100    Deposit,
101    Withdrawal,
102    Transfer,
103}
104
105/// Commands that can be applied to the banking system
106#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
107pub enum BankingCommand {
108    /// Create a new account
109    CreateAccount {
110        account_id: String,
111        initial_balance: i64,
112    },
113    /// Deposit money into an account
114    Deposit { account_id: String, amount: i64 },
115    /// Withdraw money from an account
116    Withdraw { account_id: String, amount: i64 },
117    /// Transfer money between accounts
118    Transfer {
119        from_account: String,
120        to_account: String,
121        amount: i64,
122    },
123    /// Get account balance
124    GetBalance { account_id: String },
125    /// Get account information
126    GetAccount { account_id: String },
127    /// List all accounts
128    ListAccounts,
129    /// Get transaction history
130    GetTransactionHistory {
131        account_id: Option<String>,
132        limit: Option<usize>,
133    },
134}
135
136/// Response from banking operations
137#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
138pub struct BankingResponse {
139    /// Whether the operation was successful
140    pub success: bool,
141    /// Result data (account info, balance, etc.)
142    pub data: Option<BankingData>,
143    /// Error message if operation failed
144    pub error: Option<String>,
145    /// Transaction ID for operations that create transactions
146    pub transaction_id: Option<String>,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
150pub enum BankingData {
151    Account(Account),
152    Balance(i64),
153    Accounts(Vec<Account>),
154    Transactions(Vec<Transaction>),
155}
156
157impl BankingResponse {
158    pub fn success(data: Option<BankingData>) -> Self {
159        Self {
160            success: true,
161            data,
162            error: None,
163            transaction_id: None,
164        }
165    }
166
167    pub fn success_with_transaction(data: Option<BankingData>, transaction_id: String) -> Self {
168        Self {
169            success: true,
170            data,
171            error: None,
172            transaction_id: Some(transaction_id),
173        }
174    }
175
176    pub fn error(message: String) -> Self {
177        Self {
178            success: false,
179            data: None,
180            error: Some(message),
181            transaction_id: None,
182        }
183    }
184}
185
186/// State of the banking state machine
187#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
188pub struct BankingState {
189    /// All accounts indexed by account ID
190    pub accounts: HashMap<String, Account>,
191    /// Transaction history
192    pub transactions: Vec<Transaction>,
193    /// Total number of operations performed
194    pub operation_count: u64,
195}
196
197/// Banking state machine implementation
198#[derive(Debug, Clone)]
199pub struct BankingSMR {
200    state: BankingState,
201}
202
203impl BankingSMR {
204    /// Create a new banking state machine
205    pub fn new() -> Self {
206        Self {
207            state: BankingState::default(),
208        }
209    }
210
211    /// Get the total number of accounts
212    pub fn account_count(&self) -> usize {
213        self.state.accounts.len()
214    }
215
216    /// Get the total number of transactions
217    pub fn transaction_count(&self) -> usize {
218        self.state.transactions.len()
219    }
220
221    /// Get the total number of operations performed
222    pub fn operation_count(&self) -> u64 {
223        self.state.operation_count
224    }
225
226    /// Get total value across all accounts
227    pub fn total_value(&self) -> i64 {
228        self.state
229            .accounts
230            .values()
231            .map(|account| account.balance)
232            .sum()
233    }
234
235    fn generate_transaction_id() -> String {
236        uuid::Uuid::new_v4().to_string()
237    }
238
239    fn validate_amount(amount: i64) -> Result<(), String> {
240        if amount <= 0 {
241            return Err("Amount must be positive".to_string());
242        }
243        if amount > 1_000_000_000 {
244            // Max $10M per transaction
245            return Err("Amount exceeds maximum limit".to_string());
246        }
247        Ok(())
248    }
249
250    fn validate_account_id(account_id: &str) -> Result<(), String> {
251        if account_id.is_empty() {
252            return Err("Account ID cannot be empty".to_string());
253        }
254        if account_id.len() > 50 {
255            return Err("Account ID too long".to_string());
256        }
257        Ok(())
258    }
259}
260
261impl Default for BankingSMR {
262    fn default() -> Self {
263        Self::new()
264    }
265}
266
267#[async_trait]
268impl StateMachine for BankingSMR {
269    type Command = BankingCommand;
270    type Response = BankingResponse;
271    type State = BankingState;
272
273    async fn apply_command(&mut self, command: Self::Command) -> Self::Response {
274        self.state.operation_count += 1;
275
276        match command {
277            BankingCommand::CreateAccount {
278                account_id,
279                initial_balance,
280            } => {
281                if let Err(e) = Self::validate_account_id(&account_id) {
282                    return BankingResponse::error(e);
283                }
284
285                if initial_balance < 0 {
286                    return BankingResponse::error(
287                        "Initial balance cannot be negative".to_string(),
288                    );
289                }
290
291                if self.state.accounts.contains_key(&account_id) {
292                    return BankingResponse::error("Account already exists".to_string());
293                }
294
295                let account = Account::new(account_id.clone(), initial_balance);
296                self.state.accounts.insert(account_id, account.clone());
297
298                BankingResponse::success(Some(BankingData::Account(account)))
299            }
300
301            BankingCommand::Deposit { account_id, amount } => {
302                if let Err(e) = Self::validate_amount(amount) {
303                    return BankingResponse::error(e);
304                }
305
306                let account = match self.state.accounts.get_mut(&account_id) {
307                    Some(account) => account,
308                    None => return BankingResponse::error("Account not found".to_string()),
309                };
310
311                match account.balance.checked_add(amount) {
312                    Some(new_balance) => {
313                        account.update_balance(new_balance);
314
315                        let transaction_id = Self::generate_transaction_id();
316                        let transaction = Transaction {
317                            transaction_id: transaction_id.clone(),
318                            from_account: None,
319                            to_account: Some(account_id),
320                            amount,
321                            timestamp: std::time::SystemTime::now()
322                                .duration_since(std::time::UNIX_EPOCH)
323                                .unwrap_or_default()
324                                .as_millis() as u64,
325                            transaction_type: TransactionType::Deposit,
326                        };
327                        self.state.transactions.push(transaction);
328
329                        BankingResponse::success_with_transaction(
330                            Some(BankingData::Balance(new_balance)),
331                            transaction_id,
332                        )
333                    }
334                    None => BankingResponse::error("Deposit would cause overflow".to_string()),
335                }
336            }
337
338            BankingCommand::Withdraw { account_id, amount } => {
339                if let Err(e) = Self::validate_amount(amount) {
340                    return BankingResponse::error(e);
341                }
342
343                let account = match self.state.accounts.get_mut(&account_id) {
344                    Some(account) => account,
345                    None => return BankingResponse::error("Account not found".to_string()),
346                };
347
348                if account.balance < amount {
349                    return BankingResponse::error("Insufficient funds".to_string());
350                }
351
352                let new_balance = account.balance - amount;
353                account.update_balance(new_balance);
354
355                let transaction_id = Self::generate_transaction_id();
356                let transaction = Transaction {
357                    transaction_id: transaction_id.clone(),
358                    from_account: Some(account_id),
359                    to_account: None,
360                    amount,
361                    timestamp: std::time::SystemTime::now()
362                        .duration_since(std::time::UNIX_EPOCH)
363                        .unwrap_or_default()
364                        .as_millis() as u64,
365                    transaction_type: TransactionType::Withdrawal,
366                };
367                self.state.transactions.push(transaction);
368
369                BankingResponse::success_with_transaction(
370                    Some(BankingData::Balance(new_balance)),
371                    transaction_id,
372                )
373            }
374
375            BankingCommand::Transfer {
376                from_account,
377                to_account,
378                amount,
379            } => {
380                if let Err(e) = Self::validate_amount(amount) {
381                    return BankingResponse::error(e);
382                }
383
384                if from_account == to_account {
385                    return BankingResponse::error("Cannot transfer to same account".to_string());
386                }
387
388                // Check if both accounts exist and get their current balances
389                let from_balance = match self.state.accounts.get(&from_account) {
390                    Some(account) => account.balance,
391                    None => return BankingResponse::error("Source account not found".to_string()),
392                };
393
394                if !self.state.accounts.contains_key(&to_account) {
395                    return BankingResponse::error("Destination account not found".to_string());
396                }
397
398                if from_balance < amount {
399                    return BankingResponse::error("Insufficient funds".to_string());
400                }
401
402                // Perform the transfer
403                let from_account_ref = self.state.accounts.get_mut(&from_account).unwrap();
404                from_account_ref.update_balance(from_balance - amount);
405
406                let to_account_ref = self.state.accounts.get_mut(&to_account).unwrap();
407                let to_new_balance = to_account_ref.balance + amount;
408                to_account_ref.update_balance(to_new_balance);
409
410                let transaction_id = Self::generate_transaction_id();
411                let transaction = Transaction {
412                    transaction_id: transaction_id.clone(),
413                    from_account: Some(from_account),
414                    to_account: Some(to_account),
415                    amount,
416                    timestamp: std::time::SystemTime::now()
417                        .duration_since(std::time::UNIX_EPOCH)
418                        .unwrap_or_default()
419                        .as_millis() as u64,
420                    transaction_type: TransactionType::Transfer,
421                };
422                self.state.transactions.push(transaction);
423
424                BankingResponse::success_with_transaction(None, transaction_id)
425            }
426
427            BankingCommand::GetBalance { account_id } => {
428                match self.state.accounts.get(&account_id) {
429                    Some(account) => {
430                        BankingResponse::success(Some(BankingData::Balance(account.balance)))
431                    }
432                    None => BankingResponse::error("Account not found".to_string()),
433                }
434            }
435
436            BankingCommand::GetAccount { account_id } => {
437                match self.state.accounts.get(&account_id) {
438                    Some(account) => {
439                        BankingResponse::success(Some(BankingData::Account(account.clone())))
440                    }
441                    None => BankingResponse::error("Account not found".to_string()),
442                }
443            }
444
445            BankingCommand::ListAccounts => {
446                let accounts: Vec<Account> = self.state.accounts.values().cloned().collect();
447                BankingResponse::success(Some(BankingData::Accounts(accounts)))
448            }
449
450            BankingCommand::GetTransactionHistory { account_id, limit } => {
451                let mut transactions: Vec<Transaction> = if let Some(account_id) = account_id {
452                    self.state
453                        .transactions
454                        .iter()
455                        .filter(|tx| {
456                            tx.from_account.as_ref() == Some(&account_id)
457                                || tx.to_account.as_ref() == Some(&account_id)
458                        })
459                        .cloned()
460                        .collect()
461                } else {
462                    self.state.transactions.clone()
463                };
464
465                // Sort by timestamp (newest first)
466                transactions.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
467
468                // Apply limit if specified
469                if let Some(limit) = limit {
470                    transactions.truncate(limit);
471                }
472
473                BankingResponse::success(Some(BankingData::Transactions(transactions)))
474            }
475        }
476    }
477
478    fn get_state(&self) -> Self::State {
479        self.state.clone()
480    }
481
482    fn set_state(&mut self, state: Self::State) {
483        self.state = state;
484    }
485
486    fn serialize_state(&self) -> Vec<u8> {
487        bincode::serialize(&self.state).unwrap_or_default()
488    }
489
490    fn deserialize_state(&mut self, data: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
491        self.state = bincode::deserialize(data)?;
492        Ok(())
493    }
494
495    async fn apply_commands(&mut self, commands: Vec<Self::Command>) -> Vec<Self::Response> {
496        let mut responses = Vec::with_capacity(commands.len());
497        for command in commands {
498            responses.push(self.apply_command(command).await);
499        }
500        responses
501    }
502
503    fn is_deterministic(&self) -> bool {
504        true
505    }
506}
507
508#[cfg(test)]
509mod tests {
510    use super::*;
511
512    #[tokio::test]
513    async fn test_banking_account_creation() {
514        let mut bank = BankingSMR::new();
515
516        let response = bank
517            .apply_command(BankingCommand::CreateAccount {
518                account_id: "alice".to_string(),
519                initial_balance: 1000,
520            })
521            .await;
522
523        assert!(response.success);
524        assert_eq!(bank.account_count(), 1);
525
526        // Test duplicate account creation
527        let response = bank
528            .apply_command(BankingCommand::CreateAccount {
529                account_id: "alice".to_string(),
530                initial_balance: 500,
531            })
532            .await;
533
534        assert!(!response.success);
535        assert!(response.error.as_ref().unwrap().contains("already exists"));
536    }
537
538    #[tokio::test]
539    async fn test_banking_deposit_withdraw() {
540        let mut bank = BankingSMR::new();
541
542        // Create account
543        bank.apply_command(BankingCommand::CreateAccount {
544            account_id: "alice".to_string(),
545            initial_balance: 1000,
546        })
547        .await;
548
549        // Test deposit
550        let response = bank
551            .apply_command(BankingCommand::Deposit {
552                account_id: "alice".to_string(),
553                amount: 500,
554            })
555            .await;
556
557        assert!(response.success);
558        assert!(response.transaction_id.is_some());
559
560        // Check balance
561        let response = bank
562            .apply_command(BankingCommand::GetBalance {
563                account_id: "alice".to_string(),
564            })
565            .await;
566
567        if let Some(BankingData::Balance(balance)) = response.data {
568            assert_eq!(balance, 1500);
569        } else {
570            panic!("Expected balance data");
571        }
572
573        // Test withdrawal
574        let response = bank
575            .apply_command(BankingCommand::Withdraw {
576                account_id: "alice".to_string(),
577                amount: 200,
578            })
579            .await;
580
581        assert!(response.success);
582
583        // Test insufficient funds
584        let response = bank
585            .apply_command(BankingCommand::Withdraw {
586                account_id: "alice".to_string(),
587                amount: 2000,
588            })
589            .await;
590
591        assert!(!response.success);
592        assert!(response
593            .error
594            .as_ref()
595            .unwrap()
596            .contains("Insufficient funds"));
597    }
598
599    #[tokio::test]
600    async fn test_banking_transfer() {
601        let mut bank = BankingSMR::new();
602
603        // Create accounts
604        bank.apply_command(BankingCommand::CreateAccount {
605            account_id: "alice".to_string(),
606            initial_balance: 1000,
607        })
608        .await;
609
610        bank.apply_command(BankingCommand::CreateAccount {
611            account_id: "bob".to_string(),
612            initial_balance: 500,
613        })
614        .await;
615
616        // Test successful transfer
617        let response = bank
618            .apply_command(BankingCommand::Transfer {
619                from_account: "alice".to_string(),
620                to_account: "bob".to_string(),
621                amount: 300,
622            })
623            .await;
624
625        assert!(response.success);
626        assert!(response.transaction_id.is_some());
627
628        // Check balances
629        let alice_response = bank
630            .apply_command(BankingCommand::GetBalance {
631                account_id: "alice".to_string(),
632            })
633            .await;
634        let bob_response = bank
635            .apply_command(BankingCommand::GetBalance {
636                account_id: "bob".to_string(),
637            })
638            .await;
639
640        if let Some(BankingData::Balance(alice_balance)) = alice_response.data {
641            assert_eq!(alice_balance, 700);
642        }
643
644        if let Some(BankingData::Balance(bob_balance)) = bob_response.data {
645            assert_eq!(bob_balance, 800);
646        }
647
648        // Test insufficient funds transfer
649        let response = bank
650            .apply_command(BankingCommand::Transfer {
651                from_account: "alice".to_string(),
652                to_account: "bob".to_string(),
653                amount: 1000,
654            })
655            .await;
656
657        assert!(!response.success);
658        assert!(response
659            .error
660            .as_ref()
661            .unwrap()
662            .contains("Insufficient funds"));
663    }
664
665    #[tokio::test]
666    async fn test_banking_state_serialization() {
667        let mut bank = BankingSMR::new();
668
669        // Create accounts and perform operations
670        bank.apply_command(BankingCommand::CreateAccount {
671            account_id: "alice".to_string(),
672            initial_balance: 1000,
673        })
674        .await;
675
676        bank.apply_command(BankingCommand::Deposit {
677            account_id: "alice".to_string(),
678            amount: 500,
679        })
680        .await;
681
682        // Serialize state
683        let serialized = bank.serialize_state();
684        assert!(!serialized.is_empty());
685
686        // Create new bank and deserialize
687        let mut new_bank = BankingSMR::new();
688        new_bank.deserialize_state(&serialized).unwrap();
689
690        // Verify state was restored
691        assert_eq!(new_bank.account_count(), 1);
692        assert_eq!(new_bank.transaction_count(), 1);
693        assert_eq!(new_bank.operation_count(), bank.operation_count());
694
695        let response = new_bank
696            .apply_command(BankingCommand::GetBalance {
697                account_id: "alice".to_string(),
698            })
699            .await;
700
701        if let Some(BankingData::Balance(balance)) = response.data {
702            assert_eq!(balance, 1500);
703        }
704    }
705
706    #[tokio::test]
707    async fn test_banking_transaction_history() {
708        let mut bank = BankingSMR::new();
709
710        // Create accounts
711        bank.apply_command(BankingCommand::CreateAccount {
712            account_id: "alice".to_string(),
713            initial_balance: 1000,
714        })
715        .await;
716
717        bank.apply_command(BankingCommand::CreateAccount {
718            account_id: "bob".to_string(),
719            initial_balance: 500,
720        })
721        .await;
722
723        // Perform several transactions
724        bank.apply_command(BankingCommand::Deposit {
725            account_id: "alice".to_string(),
726            amount: 200,
727        })
728        .await;
729
730        bank.apply_command(BankingCommand::Transfer {
731            from_account: "alice".to_string(),
732            to_account: "bob".to_string(),
733            amount: 300,
734        })
735        .await;
736
737        // Get all transaction history
738        let response = bank
739            .apply_command(BankingCommand::GetTransactionHistory {
740                account_id: None,
741                limit: None,
742            })
743            .await;
744
745        if let Some(BankingData::Transactions(transactions)) = response.data {
746            assert_eq!(transactions.len(), 2);
747        }
748
749        // Get Alice's transaction history
750        let response = bank
751            .apply_command(BankingCommand::GetTransactionHistory {
752                account_id: Some("alice".to_string()),
753                limit: None,
754            })
755            .await;
756
757        if let Some(BankingData::Transactions(transactions)) = response.data {
758            assert_eq!(transactions.len(), 2); // Deposit + Transfer
759        }
760    }
761}