seed.rs 6.2 KB

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