lifecycle.rs 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. //! Account lifecycle: create, freeze, unfreeze, close.
  2. //!
  3. //! Accounts are append-only and versioned: each mutation appends a new version
  4. //! rather than editing in place. Freeze/close guards are validate-time and
  5. //! best-effort under concurrency (see the dumb-storage ADR).
  6. //!
  7. //! Freeze, unfreeze, and close are the same version-bump-plus-event shape; they
  8. //! delegate it to [`Ledger::transition`](super::transition), which carries the
  9. //! write-ahead / crash-repair path. Each method here supplies only the flag
  10. //! mutation, the lifecycle event, and any transition-specific guard.
  11. use tracing::instrument;
  12. use kuatia_core::{AccountFlags, AccountId, PostingFilter};
  13. use kuatia_storage::events::{LedgerEvent, LedgerEventKind};
  14. use super::{Ledger, now_millis};
  15. use crate::error::LedgerError;
  16. impl Ledger {
  17. /// Create a new account and emit an AccountCreated event.
  18. pub async fn create_account(&self, account: kuatia_core::Account) -> Result<(), LedgerError> {
  19. let id = account.id;
  20. if self.store.create_account(account).await? == 0 {
  21. return Err(LedgerError::AccountAlreadyExists(id));
  22. }
  23. self.store
  24. .append_event(&LedgerEvent {
  25. seq: 0,
  26. timestamp: now_millis()?,
  27. kind: LedgerEventKind::AccountCreated { account_id: id },
  28. })
  29. .await?;
  30. Ok(())
  31. }
  32. /// Freeze an account, preventing all transfers.
  33. #[instrument(skip(self), name = "ledger.freeze")]
  34. pub async fn freeze(&self, id: &AccountId) -> Result<(), LedgerError> {
  35. self.transition(
  36. id,
  37. |flags| *flags |= AccountFlags::FROZEN,
  38. |account_id, version| LedgerEventKind::AccountFrozen {
  39. account_id,
  40. version,
  41. },
  42. )
  43. .await
  44. }
  45. /// Unfreeze a previously frozen account.
  46. #[instrument(skip(self), name = "ledger.unfreeze")]
  47. pub async fn unfreeze(&self, id: &AccountId) -> Result<(), LedgerError> {
  48. self.transition(
  49. id,
  50. |flags| flags.remove(AccountFlags::FROZEN),
  51. |account_id, version| LedgerEventKind::AccountUnfrozen {
  52. account_id,
  53. version,
  54. },
  55. )
  56. .await
  57. }
  58. /// Close an account. Must have no live postings.
  59. #[instrument(skip(self), name = "ledger.close")]
  60. pub async fn close(&self, id: &AccountId) -> Result<(), LedgerError> {
  61. // Emptiness is close's own guard, checked before the transition's
  62. // write-ahead so a non-empty account records nothing. A closed account
  63. // holds no live postings, so this ordering still surfaces
  64. // `AccountAlreadyClosed` (from `transition`) for a re-close.
  65. if self.has_live_postings(id).await? {
  66. return Err(LedgerError::AccountNotEmpty(*id));
  67. }
  68. self.transition(
  69. id,
  70. |flags| {
  71. *flags |= AccountFlags::CLOSED;
  72. flags.remove(AccountFlags::FROZEN);
  73. },
  74. |account_id, version| LedgerEventKind::AccountClosed {
  75. account_id,
  76. version,
  77. },
  78. )
  79. .await
  80. }
  81. /// Whether `account` (exact base id and subaccount) has any live posting: one
  82. /// that is active or reserved by an in-flight saga. Spent postings do not
  83. /// count. This is the emptiness test [`close`](Self::close) gates on, and the
  84. /// inflight layer uses it to decide when a drained hold can be closed.
  85. #[instrument(skip(self), name = "ledger.has_live_postings")]
  86. pub async fn has_live_postings(&self, account: &AccountId) -> Result<bool, LedgerError> {
  87. Ok(!self
  88. .store
  89. .get_postings_by_account(account.id, Some(account.sub), None, PostingFilter::Live)
  90. .await?
  91. .is_empty())
  92. }
  93. }