Explorar el Código

Add runnable examples for connecting to and using a ledger

New users had to reverse-engineer usage from integration tests. Add
self-contained example programs that connect to a real SQLite-backed
ledger via sqlx and walk through the core operations.

- create_accounts: open a ledger and create user/system/external accounts.
- fund_and_trade: deposit two assets into two accounts, then swap them in
  one atomic two-movement transfer.
- withdraw: fund an account, then move value out to the external boundary.

Also add an Account::new(id, policy) convenience constructor so callers
avoid a seven-field struct literal for the common case, and wire
kuatia-storage-sql + sqlx as dev-dependencies of kuatia for the examples.
Cesar Rodas hace 1 semana
padre
commit
dad6c656b9

+ 2 - 0
Cargo.lock

@@ -586,9 +586,11 @@ dependencies = [
  "async-trait",
  "kuatia-core",
  "kuatia-storage",
+ "kuatia-storage-sql",
  "kuatia-types",
  "legend",
  "serde",
+ "sqlx",
  "tokio",
  "tracing",
 ]

+ 15 - 0
crates/kuatia-types/src/lib.rs

@@ -859,6 +859,21 @@ pub struct Account {
 }
 
 impl Account {
+    /// Create a version-1 account with the given policy: no flags, the default
+    /// book, and empty user data / metadata. Convenience for the common case —
+    /// set the other fields explicitly when you need them.
+    pub fn new(id: AccountId, policy: AccountPolicy) -> Self {
+        Self {
+            id,
+            version: 1,
+            policy,
+            flags: AccountFlags::empty(),
+            book: DEFAULT_BOOK,
+            user_data: UserData::default(),
+            metadata: Metadata::new(),
+        }
+    }
+
     /// Returns `true` if the account has the `FROZEN` flag set.
     pub fn is_frozen(&self) -> bool {
         self.flags.contains(AccountFlags::FROZEN)

+ 3 - 0
crates/kuatia/Cargo.toml

@@ -20,3 +20,6 @@ tracing = "0.1"
 
 [dev-dependencies]
 tokio = { version = "1", features = ["full"] }
+# For the runnable examples — connect to a real SQLite-backed ledger.
+kuatia-storage-sql = { path = "../kuatia-storage-sql" }
+sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio", "any", "sqlite"] }

+ 15 - 1
crates/kuatia/README.md

@@ -31,7 +31,7 @@ let receipt = ledger.commit(transfer).await?;
 1. **Resolve** — convert Transfer intent into concrete Envelope
 2. **Reserve** — batch CAS: Active → PendingInactive
 3. **Validate** — pure `validate_and_plan()`
-4. **Finalize** — PendingInactive → Inactive, create new postings, emit event
+4. **Finalize** — one atomic `commit_transfer`: deactivate consumed postings, create new ones, persist the transfer record and account index, and emit the event — all in a single transaction
 
 ### Atomic commit
 
@@ -69,6 +69,20 @@ legend! {
 }
 ```
 
+## Examples
+
+Runnable programs in [`examples/`](examples/) connect to a real SQLite-backed
+ledger (via `sqlx`) and walk through the core operations:
+
+```sh
+cargo run -p kuatia --example create_accounts   # create user/system/external accounts
+cargo run -p kuatia --example fund_and_trade     # fund two accounts in different assets, then swap
+cargo run -p kuatia --example withdraw           # fund an account, then withdraw out of the ledger
+```
+
+Each opens an in-memory SQLite database (`sqlite::memory:`); point the
+connection string at a file or a Postgres URL for a persistent ledger.
+
 ## See also
 
 - [doc/accounting-mapping.md](../../doc/accounting-mapping.md) — how classical

+ 67 - 0
crates/kuatia/examples/create_accounts.rs

@@ -0,0 +1,67 @@
+//! Connect to a SQLite-backed ledger and create accounts.
+//!
+//! Run with:
+//! ```sh
+//! cargo run -p kuatia --example create_accounts
+//! ```
+
+use std::collections::BTreeMap;
+use std::sync::Arc;
+
+use kuatia::ledger::Ledger;
+use kuatia_core::*;
+use kuatia_storage_sql::SqlStore;
+
+#[tokio::main]
+async fn main() -> Result<(), Box<dyn std::error::Error>> {
+    let ledger = connect().await?;
+
+    // The common case is one line: a version-1 account with the given policy.
+    ledger
+        .create_account(Account::new(AccountId::new(1), AccountPolicy::NoOverdraft))
+        .await?;
+    ledger
+        .create_account(Account::new(AccountId::new(2), AccountPolicy::NoOverdraft))
+        .await?;
+    // A system account (fees, settlement, market-making) — no balance floor.
+    ledger
+        .create_account(Account::new(AccountId::new(50), AccountPolicy::SystemAccount))
+        .await?;
+
+    // The same thing spelled out, so you can see every field of an `Account`.
+    // This boundary account is where value enters/leaves the ledger.
+    let external = Account {
+        id: AccountId::new(99),
+        version: 1,                             // accounts always start at version 1
+        policy: AccountPolicy::ExternalAccount, // boundary for deposits/withdrawals
+        flags: AccountFlags::empty(),           // not frozen, not closed
+        book: DEFAULT_BOOK,                     // the implicit default book
+        user_data: UserData::default(),         // fixed-width correlation slots
+        metadata: BTreeMap::new(),              // free-form key/value metadata
+    };
+    ledger.create_account(external).await?;
+
+    // Read them back (latest version of each).
+    println!("accounts:");
+    let mut accounts = ledger.list_accounts().await?;
+    accounts.sort_by_key(|a| a.id.0);
+    for a in &accounts {
+        println!("  {:?}  policy={:?}  v{}", a.id, a.policy, a.version);
+    }
+
+    Ok(())
+}
+
+/// Open a fresh in-memory SQLite database, run migrations, and wrap it in a
+/// `Ledger`. Point the connection string at a file (e.g.
+/// `"sqlite://ledger.db?mode=rwc"`) or a Postgres URL for a persistent ledger.
+async fn connect() -> Result<Arc<Ledger>, Box<dyn std::error::Error>> {
+    sqlx::any::install_default_drivers();
+    let pool = sqlx::any::AnyPoolOptions::new()
+        .max_connections(1)
+        .connect("sqlite::memory:")
+        .await?;
+    let store = SqlStore::new(pool);
+    store.migrate().await?;
+    Ok(Arc::new(Ledger::new(store)))
+}

+ 100 - 0
crates/kuatia/examples/fund_and_trade.rs

@@ -0,0 +1,100 @@
+//! Fund two accounts with different assets, then trade between them atomically.
+//!
+//! Run with:
+//! ```sh
+//! cargo run -p kuatia --example fund_and_trade
+//! ```
+
+use std::sync::Arc;
+
+use kuatia::ledger::Ledger;
+use kuatia_core::*;
+use kuatia_storage_sql::SqlStore;
+
+#[tokio::main]
+async fn main() -> Result<(), Box<dyn std::error::Error>> {
+    let ledger = connect().await?;
+
+    let alice = AccountId::new(1);
+    let bob = AccountId::new(2);
+    let external = AccountId::new(99);
+    let usd = AssetId::new(1);
+    let eur = AssetId::new(2);
+
+    // Two-decimal money: `money.parse("100.00")` -> Cent in minor units.
+    let money = Amount::new(2);
+
+    ledger
+        .create_account(Account::new(alice, AccountPolicy::NoOverdraft))
+        .await?;
+    ledger
+        .create_account(Account::new(bob, AccountPolicy::NoOverdraft))
+        .await?;
+    ledger
+        .create_account(Account::new(external, AccountPolicy::ExternalAccount))
+        .await?;
+
+    // Fund: $100.00 to Alice, €90.00 to Bob.
+    ledger
+        .commit(
+            TransferBuilder::new()
+                .deposit(alice, usd, money.parse("100.00")?, external)?
+                .build(),
+        )
+        .await?;
+    ledger
+        .commit(
+            TransferBuilder::new()
+                .deposit(bob, eur, money.parse("90.00")?, external)?
+                .build(),
+        )
+        .await?;
+
+    println!("after funding:");
+    print_balances(&ledger, alice, bob, usd, eur).await?;
+
+    // Trade: Alice gives 100 USD to Bob; Bob gives 90 EUR to Alice. Both legs
+    // settle in one atomic transfer — each asset is conserved independently.
+    let trade = TransferBuilder::new()
+        .movement(alice, bob, usd, money.parse("100.00")?)
+        .movement(bob, alice, eur, money.parse("90.00")?)
+        .build();
+    ledger.commit(trade).await?;
+
+    println!("after trade:");
+    print_balances(&ledger, alice, bob, usd, eur).await?;
+
+    Ok(())
+}
+
+async fn print_balances(
+    ledger: &Arc<Ledger>,
+    alice: AccountId,
+    bob: AccountId,
+    usd: AssetId,
+    eur: AssetId,
+) -> Result<(), Box<dyn std::error::Error>> {
+    let money = Amount::new(2);
+    println!(
+        "  alice: {} USD, {} EUR",
+        money.format(ledger.balance(&alice, &usd).await?),
+        money.format(ledger.balance(&alice, &eur).await?),
+    );
+    println!(
+        "  bob:   {} USD, {} EUR",
+        money.format(ledger.balance(&bob, &usd).await?),
+        money.format(ledger.balance(&bob, &eur).await?),
+    );
+    Ok(())
+}
+
+async fn connect() -> Result<Arc<Ledger>, Box<dyn std::error::Error>> {
+    sqlx::any::install_default_drivers();
+    let pool = sqlx::any::AnyPoolOptions::new()
+        .max_connections(1)
+        .connect("sqlite::memory:")
+        .await?;
+    let store = SqlStore::new(pool);
+    store.migrate().await?;
+    Ok(Arc::new(Ledger::new(store)))
+}

+ 75 - 0
crates/kuatia/examples/withdraw.rs

@@ -0,0 +1,75 @@
+//! Fund an account, then withdraw value out of the ledger.
+//!
+//! Run with:
+//! ```sh
+//! cargo run -p kuatia --example withdraw
+//! ```
+
+use std::sync::Arc;
+
+use kuatia::ledger::Ledger;
+use kuatia_core::*;
+use kuatia_storage_sql::SqlStore;
+
+#[tokio::main]
+async fn main() -> Result<(), Box<dyn std::error::Error>> {
+    let ledger = connect().await?;
+
+    let alice = AccountId::new(1);
+    let external = AccountId::new(99);
+    let usd = AssetId::new(1);
+    let money = Amount::new(2);
+
+    ledger
+        .create_account(Account::new(alice, AccountPolicy::NoOverdraft))
+        .await?;
+    ledger
+        .create_account(Account::new(external, AccountPolicy::ExternalAccount))
+        .await?;
+
+    // Fund Alice with $100.00.
+    ledger
+        .commit(
+            TransferBuilder::new()
+                .deposit(alice, usd, money.parse("100.00")?, external)?
+                .build(),
+        )
+        .await?;
+    println!(
+        "after deposit:  alice = {} USD",
+        money.format(ledger.balance(&alice, &usd).await?)
+    );
+
+    // Withdraw $30.00 from Alice out to the external boundary account.
+    ledger
+        .commit(
+            TransferBuilder::new()
+                .withdraw(alice, usd, money.parse("30.00")?, external)
+                .build(),
+        )
+        .await?;
+    println!(
+        "after withdraw: alice = {} USD",
+        money.format(ledger.balance(&alice, &usd).await?)
+    );
+
+    // The external account carries the offset (negative) side: the mirror of the
+    // value that currently sits inside the ledger.
+    println!(
+        "external boundary: {} USD",
+        money.format(ledger.balance(&external, &usd).await?)
+    );
+
+    Ok(())
+}
+
+async fn connect() -> Result<Arc<Ledger>, Box<dyn std::error::Error>> {
+    sqlx::any::install_default_drivers();
+    let pool = sqlx::any::AnyPoolOptions::new()
+        .max_connections(1)
+        .connect("sqlite::memory:")
+        .await?;
+    let store = SqlStore::new(pool);
+    store.migrate().await?;
+    Ok(Arc::new(Ledger::new(store)))
+}