Skip to main content

ousia_ledger/
lib.rs

1// ledger/src/lib.rs
2pub mod adapters;
3pub mod asset;
4pub mod balance;
5pub mod error;
6pub mod holding;
7pub mod money;
8pub mod transaction;
9pub mod value_object;
10
11pub use asset::Asset;
12pub use balance::Balance;
13use chrono::{DateTime, Utc};
14pub use error::MoneyError;
15pub use holding::{Holding, Portfolio};
16pub use money::{ExecutionPlan, LedgerContext, Money, MoneySlice, Operation, TransactionContext};
17pub use transaction::Transaction;
18pub use value_object::{ValueObject, ValueObjectState};
19
20use async_trait::async_trait;
21use std::sync::Arc;
22use uuid::Uuid;
23
24pub(crate) fn hash_idempotency_key(key: &str) -> String {
25    blake3::hash(key.as_bytes()).to_hex().to_string()
26}
27
28/// Internal ledger adapter trait
29#[async_trait]
30pub trait LedgerAdapter: Send + Sync {
31    /// Execute the complete operation plan atomically.
32    /// Implementors MUST:
33    /// 1. BEGIN a database transaction
34    /// 2. SELECT FOR UPDATE the required value objects (from `locks`)
35    /// 3. Verify sum >= required amount — return InsufficientFunds if not
36    /// 4. Execute all operations
37    /// 5. COMMIT on success, ROLLBACK on any error
38    async fn execute_plan(
39        &self,
40        plan: &ExecutionPlan,
41        locks: &[(Uuid, Uuid, u64)],
42    ) -> Result<(), MoneyError>;
43
44    // READ OPERATIONS
45    async fn get_balance(&self, asset_id: Uuid, owner: Uuid) -> Result<Balance, MoneyError>;
46    async fn get_transaction(&self, tx_id: Uuid) -> Result<Transaction, MoneyError>;
47    async fn get_transactions_for_owner(
48        &self,
49        owner: Uuid,
50        timespan: &[DateTime<Utc>; 2],
51    ) -> Result<Vec<Transaction>, MoneyError>;
52    async fn check_idempotency_key(&self, key: &str) -> Result<(), MoneyError>;
53    async fn get_transaction_by_idempotency_key(
54        &self,
55        key: &str,
56    ) -> Result<Transaction, MoneyError>;
57    async fn get_asset(&self, code: &str) -> Result<Asset, MoneyError>;
58    async fn create_asset(&self, asset: Asset) -> Result<(), MoneyError>;
59
60    /// All assets held by `owner` with a non-zero balance.
61    async fn get_holdings(&self, owner: Uuid) -> Result<Vec<Holding>, MoneyError>;
62
63    /// All transactions that touched `asset_id` within `timespan`.
64    async fn get_transactions_for_asset(
65        &self,
66        asset_id: Uuid,
67        timespan: &[DateTime<Utc>; 2],
68    ) -> Result<Vec<Transaction>, MoneyError>;
69}
70
71/// Initialize the ledger system with an adapter
72pub struct LedgerSystem {
73    adapter: Arc<dyn LedgerAdapter>,
74}
75
76impl LedgerSystem {
77    pub fn new(adapter: Box<dyn LedgerAdapter>) -> Self {
78        Self {
79            adapter: adapter.into(),
80        }
81    }
82
83    /// Get adapter reference
84    pub fn adapter(&self) -> &dyn LedgerAdapter {
85        self.adapter.as_ref()
86    }
87
88    /// Get adapter Arc (for creating contexts)
89    pub fn adapter_arc(&self) -> Arc<dyn LedgerAdapter> {
90        Arc::clone(&self.adapter)
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn test_asset_conversion() {
100        let usd = Asset::new("USD", 10_000, 2);
101        assert_eq!(usd.to_internal(100.50), 10050);
102        assert_eq!(usd.to_display(10050), 100.50);
103
104        let eth = Asset::new("ETH", 1_000_000_000_000_000_000u64, 18);
105        let one_eth = 1_000_000_000_000_000_000u64;
106        assert_eq!(eth.to_display(one_eth), 1.0);
107    }
108
109    #[test]
110    fn test_value_object_states() {
111        assert!(matches!(ValueObjectState::Alive, ValueObjectState::Alive));
112        assert!(matches!(
113            ValueObjectState::Reserved,
114            ValueObjectState::Reserved
115        ));
116        assert!(matches!(ValueObjectState::Burned, ValueObjectState::Burned));
117    }
118}