seed.rs 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. //! Demo data. Builds an in-memory ledger with a handful of accounts, funds
  2. //! them from an external boundary account, then runs payments and a
  3. //! multi-asset trade so the dashboard has something to visualize.
  4. use std::error::Error;
  5. use std::sync::Arc;
  6. use kuatia::ledger::Ledger;
  7. use kuatia_core::{Account, AccountId, Amount, AssetId, Cent, TransferBuilder};
  8. use kuatia_storage_sql::SqlStore;
  9. use crate::assets::{BTC, EUR, USD};
  10. /// Well-known account ids used by the demo.
  11. pub const TREASURY: AccountId = AccountId::new(1);
  12. pub const EXTERNAL: AccountId = AccountId::new(99);
  13. pub const ALICE: AccountId = AccountId::new(100);
  14. /// A subaccount of Alice: an earmarked savings bucket under the same base id,
  15. /// with its own balance that is never summed into Alice's main account.
  16. pub const ALICE_SAVINGS: AccountId = AccountId::with_sub(100, 1);
  17. pub const BOB: AccountId = AccountId::new(101);
  18. pub const CAROL: AccountId = AccountId::new(102);
  19. pub const MERCHANT: AccountId = AccountId::new(103);
  20. /// Human-readable labels for the seeded accounts, surfaced by the API so the
  21. /// frontend can show names instead of raw ids. Labels are per base account;
  22. /// a subaccount (an inflight hold) shares its base account's label.
  23. pub fn account_label(id: AccountId) -> Option<&'static str> {
  24. Some(match id.base() {
  25. TREASURY => "Treasury",
  26. EXTERNAL => "External",
  27. ALICE => "Alice",
  28. ALICE_SAVINGS => "Alice / Savings",
  29. BOB => "Bob",
  30. CAROL => "Carol",
  31. MERCHANT => "Merchant",
  32. _ => return None,
  33. })
  34. }
  35. /// Connect to the ledger database at `db_url`, create the schema, and run
  36. /// recovery. The URL scheme selects the backend (e.g. `sqlite::memory:`,
  37. /// `sqlite://kuatia.db`, `postgres://user:pass@host/db`).
  38. ///
  39. /// The pool is capped at a single connection: `sqlite::memory:` gives each
  40. /// connection its own separate database, so more than one would split the
  41. /// ledger; one connection is also fine for a low-traffic dashboard on a file or
  42. /// Postgres backend.
  43. pub async fn connect(db_url: &str) -> Result<Arc<Ledger>, Box<dyn Error>> {
  44. sqlx::any::install_default_drivers();
  45. let pool = sqlx::any::AnyPoolOptions::new()
  46. .max_connections(1)
  47. .connect(&sqlite_creatable(db_url))
  48. .await?;
  49. let store = SqlStore::new(pool);
  50. store.migrate().await?;
  51. let ledger = Arc::new(Ledger::new(store));
  52. ledger.recover().await?;
  53. Ok(ledger)
  54. }
  55. /// A SQLite backend will not create a missing file unless the URL asks for it,
  56. /// so add `mode=rwc` to a file-backed `sqlite:` URL that does not already set a
  57. /// mode. In-memory and non-SQLite URLs pass through unchanged.
  58. fn sqlite_creatable(db_url: &str) -> String {
  59. if !db_url.starts_with("sqlite:") || db_url.contains(":memory:") || db_url.contains("mode=") {
  60. return db_url.to_string();
  61. }
  62. let sep = if db_url.contains('?') { '&' } else { '?' };
  63. format!("{db_url}{sep}mode=rwc")
  64. }
  65. /// Seed the demo data only if the ledger has no accounts yet. Returns `true` if
  66. /// it seeded, `false` if the ledger was already populated (so re-running with
  67. /// `--seed` against a persistent database is a safe no-op rather than a
  68. /// duplicate-id error).
  69. pub async fn seed_if_empty(ledger: &Arc<Ledger>) -> Result<bool, Box<dyn Error>> {
  70. if !ledger.list_accounts().await?.is_empty() {
  71. return Ok(false);
  72. }
  73. populate(ledger).await?;
  74. Ok(true)
  75. }
  76. /// Populate the ledger with demo accounts and a spread of transfers.
  77. pub async fn populate(ledger: &Arc<Ledger>) -> Result<(), Box<dyn Error>> {
  78. // Two-decimal assets (USD, EUR) and an 8-decimal asset (BTC).
  79. let fiat = Amount::new(2);
  80. let btc = Amount::new(8);
  81. // Treasury and the external boundary permit overdraft (they hold the
  82. // negative side of issuance/deposits); the user accounts forbid it.
  83. create(ledger, TREASURY, false).await?;
  84. create(ledger, EXTERNAL, false).await?;
  85. create(ledger, ALICE, true).await?;
  86. create(ledger, ALICE_SAVINGS, true).await?;
  87. create(ledger, BOB, true).await?;
  88. // Carol may overdraw (no floor under the single-flag model).
  89. create(ledger, CAROL, false).await?;
  90. create(ledger, MERCHANT, true).await?;
  91. // Fund accounts from the external boundary.
  92. deposit(ledger, ALICE, USD, fiat.parse("1000.00")?).await?;
  93. deposit(ledger, BOB, EUR, fiat.parse("500.00")?).await?;
  94. deposit(ledger, ALICE, BTC, btc.parse("0.50000000")?).await?;
  95. deposit(ledger, CAROL, USD, fiat.parse("200.00")?).await?;
  96. // Ordinary payments between held balances.
  97. pay(ledger, ALICE, BOB, USD, fiat.parse("150.00")?).await?;
  98. pay(ledger, BOB, MERCHANT, EUR, fiat.parse("80.00")?).await?;
  99. pay(ledger, ALICE, MERCHANT, BTC, btc.parse("0.10000000")?).await?;
  100. // Carol spends past her balance, into overdraft.
  101. pay(ledger, CAROL, MERCHANT, USD, fiat.parse("250.00")?).await?;
  102. // Alice earmarks part of her balance into her savings subaccount. The two
  103. // balances stay segregated under the same base id.
  104. pay(ledger, ALICE, ALICE_SAVINGS, USD, fiat.parse("300.00")?).await?;
  105. // Atomic multi-asset trade: Alice buys EUR from Bob with USD.
  106. let trade = TransferBuilder::new()
  107. .pay(ALICE, BOB, USD, fiat.parse("100.00")?)
  108. .pay(BOB, ALICE, EUR, fiat.parse("90.00")?)
  109. .build();
  110. ledger.commit(trade).await?;
  111. Ok(())
  112. }
  113. async fn create(
  114. ledger: &Arc<Ledger>,
  115. id: AccountId,
  116. debit_must_not_exceed_credit: bool,
  117. ) -> Result<(), Box<dyn Error>> {
  118. let account = if debit_must_not_exceed_credit {
  119. Account::debit_must_not_exceed_credit(id)
  120. } else {
  121. Account::new(id)
  122. };
  123. ledger.create_account(account).await?;
  124. Ok(())
  125. }
  126. async fn deposit(
  127. ledger: &Arc<Ledger>,
  128. to: AccountId,
  129. asset: AssetId,
  130. amount: Cent,
  131. ) -> Result<(), Box<dyn Error>> {
  132. let transfer = TransferBuilder::new()
  133. .deposit(to, asset, amount, EXTERNAL)?
  134. .build();
  135. ledger.commit(transfer).await?;
  136. Ok(())
  137. }
  138. async fn pay(
  139. ledger: &Arc<Ledger>,
  140. from: AccountId,
  141. to: AccountId,
  142. asset: AssetId,
  143. amount: Cent,
  144. ) -> Result<(), Box<dyn Error>> {
  145. let transfer = TransferBuilder::new().pay(from, to, asset, amount).build();
  146. ledger.commit(transfer).await?;
  147. Ok(())
  148. }