lib.rs 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967
  1. //! Domain types for the ledger.
  2. //!
  3. //! These types model the UTXO-style ledger where value is held as **postings** —
  4. //! signed amounts owned by exactly one account. An account's balance is simply the
  5. //! sum of its active postings, which eliminates the need for running balance fields
  6. //! and makes the system trivially auditable by replaying the transfer log.
  7. pub mod autoid;
  8. use serde::{Deserialize, Serialize};
  9. use std::collections::BTreeMap;
  10. use std::fmt;
  11. // ---------------------------------------------------------------------------
  12. // ToBytes trait
  13. // ---------------------------------------------------------------------------
  14. /// Deterministic binary serialization. Every domain type can produce its
  15. /// canonical byte representation.
  16. pub trait ToBytes {
  17. /// Returns the canonical byte representation of this value.
  18. fn to_bytes(&self) -> Vec<u8>;
  19. }
  20. // ---------------------------------------------------------------------------
  21. // Binary encoding helpers — big-endian, deterministic
  22. // ---------------------------------------------------------------------------
  23. /// Version byte prepended to canonical serializations for forward compatibility.
  24. /// Bumped to 2 when `Cent` moved to a fixed 16-byte canonical encoding (ADR-0011).
  25. pub const CANONICAL_VERSION: u8 = 2;
  26. /// Append a `u16` in big-endian to `buf`.
  27. pub fn write_u16(buf: &mut Vec<u8>, v: u16) {
  28. buf.extend_from_slice(&v.to_be_bytes());
  29. }
  30. /// Append a `u32` in big-endian to `buf`.
  31. pub fn write_u32(buf: &mut Vec<u8>, v: u32) {
  32. buf.extend_from_slice(&v.to_be_bytes());
  33. }
  34. /// Append a `u64` in big-endian to `buf`.
  35. pub fn write_u64(buf: &mut Vec<u8>, v: u64) {
  36. buf.extend_from_slice(&v.to_be_bytes());
  37. }
  38. /// Append an `i64` in big-endian to `buf`.
  39. pub fn write_i64(buf: &mut Vec<u8>, v: i64) {
  40. buf.extend_from_slice(&v.to_be_bytes());
  41. }
  42. /// Append a `u128` in big-endian to `buf`.
  43. pub fn write_u128(buf: &mut Vec<u8>, v: u128) {
  44. buf.extend_from_slice(&v.to_be_bytes());
  45. }
  46. // ---------------------------------------------------------------------------
  47. // Identifiers
  48. // ---------------------------------------------------------------------------
  49. /// Stable account identity. Used in all public APIs.
  50. #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
  51. pub struct AccountId(pub i64);
  52. /// Pairs an [`AccountId`] with a snapshot hash — the double-SHA256 of the
  53. /// account's state at a point in time. Stored on [`Transfer`] to record which
  54. /// account versions a transfer was executed against. Internal type — the
  55. /// public API uses [`AccountId`].
  56. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  57. pub struct AccountSnapshotId {
  58. /// The account this snapshot belongs to.
  59. pub account: AccountId,
  60. /// Double-SHA256 of the account's state at the time of the snapshot.
  61. pub snapshot_id: [u8; 32],
  62. }
  63. /// Identifies an asset (USD, EUR, BTC, …). Conservation is enforced per asset,
  64. /// so each asset is an independent conservation boundary.
  65. #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
  66. pub struct AssetId(pub u32);
  67. /// Content-addressed transfer identifier — the double-SHA256 of the canonical
  68. /// serialization. This makes the id both the idempotency key and the
  69. /// tamper-evidence artifact.
  70. #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
  71. pub struct EnvelopeId(pub [u8; 32]);
  72. /// Uniquely identifies a posting within the ledger. The `(transfer, index)` pair
  73. /// ties every posting back to the transfer that created it, which is the basis
  74. /// of the provenance graph.
  75. #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
  76. pub struct PostingId {
  77. /// The transfer that created this posting.
  78. pub transfer: EnvelopeId,
  79. /// Zero-based position within the transfer's created postings.
  80. pub index: u16,
  81. }
  82. // ---------------------------------------------------------------------------
  83. // Cent — re-exported from kuatia-money (swappable integer backing)
  84. // ---------------------------------------------------------------------------
  85. pub use kuatia_money::{Amount, Cent, OverflowError, ParseAmountError};
  86. impl ToBytes for Cent {
  87. fn to_bytes(&self) -> Vec<u8> {
  88. self.to_canonical_bytes().to_vec()
  89. }
  90. }
  91. // ---------------------------------------------------------------------------
  92. // Debug / Display impls for identifiers
  93. // ---------------------------------------------------------------------------
  94. impl fmt::Debug for AccountId {
  95. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  96. write!(f, "AccountId({})", self.0)
  97. }
  98. }
  99. impl fmt::Debug for AssetId {
  100. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  101. write!(f, "AssetId({:#010x})", self.0)
  102. }
  103. }
  104. impl fmt::Debug for EnvelopeId {
  105. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  106. write!(f, "EnvelopeId({})", hex(&self.0))
  107. }
  108. }
  109. impl fmt::Debug for PostingId {
  110. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  111. f.debug_struct("PostingId")
  112. .field("transfer", &self.transfer)
  113. .field("index", &self.index)
  114. .finish()
  115. }
  116. }
  117. fn hex(bytes: &[u8]) -> String {
  118. bytes.iter().map(|b| format!("{b:02x}")).collect()
  119. }
  120. // ---------------------------------------------------------------------------
  121. // Identifier constructors
  122. // ---------------------------------------------------------------------------
  123. impl Default for AccountId {
  124. fn default() -> Self {
  125. // Process-global generator: a per-thread one could mint the same id on
  126. // two threads within a millisecond, yielding duplicate account ids.
  127. static GEN: crate::autoid::AutoId = crate::autoid::AutoId::new();
  128. Self(GEN.next())
  129. }
  130. }
  131. impl AccountId {
  132. /// Create an `AccountId` from an `i64`.
  133. pub const fn new(id: i64) -> Self {
  134. Self(id)
  135. }
  136. }
  137. impl From<AccountSnapshotId> for AccountId {
  138. fn from(snap: AccountSnapshotId) -> Self {
  139. snap.account
  140. }
  141. }
  142. impl AssetId {
  143. /// Create an `AssetId` from a `u32`.
  144. pub const fn new(id: u32) -> Self {
  145. Self(id)
  146. }
  147. }
  148. /// Identifies a book — a named scope for transfers.
  149. #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
  150. pub struct BookId(pub i64);
  151. impl fmt::Debug for BookId {
  152. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  153. write!(f, "BookId({})", self.0)
  154. }
  155. }
  156. /// The implicit book used when a transfer does not name one. Fixed so that two
  157. /// otherwise-identical transfers hash to the same [`EnvelopeId`] — a random
  158. /// default would break content-addressed idempotency.
  159. pub const DEFAULT_BOOK: BookId = BookId(0);
  160. impl Default for BookId {
  161. /// Deterministic: returns [`DEFAULT_BOOK`]. Use [`BookId::generate`] to mint
  162. /// a fresh unique id for a real book.
  163. fn default() -> Self {
  164. DEFAULT_BOOK
  165. }
  166. }
  167. impl BookId {
  168. /// Create a `BookId` from an `i64`.
  169. pub const fn new(id: i64) -> Self {
  170. Self(id)
  171. }
  172. /// Mint a fresh, process-unique book id. Unlike [`Default`], this is not
  173. /// stable across calls — use it when creating a new [`Book`], never for the
  174. /// implicit book of a transfer.
  175. pub fn generate() -> Self {
  176. // Process-global so the "process-unique" contract holds across threads;
  177. // a per-thread generator can repeat an id on another thread.
  178. static GEN: crate::autoid::AutoId = crate::autoid::AutoId::new();
  179. Self(GEN.next())
  180. }
  181. }
  182. /// Identifies a reservation — the owner token stamped on a posting while it is
  183. /// `PendingInactive`, so only the saga that reserved it may finalize or release it.
  184. #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
  185. pub struct ReservationId(pub i64);
  186. impl fmt::Debug for ReservationId {
  187. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  188. write!(f, "ReservationId({})", self.0)
  189. }
  190. }
  191. impl ReservationId {
  192. /// Create a `ReservationId` from an `i64`.
  193. pub const fn new(id: i64) -> Self {
  194. Self(id)
  195. }
  196. }
  197. impl Default for ReservationId {
  198. fn default() -> Self {
  199. // One process-global generator, not one per thread: its atomic counter
  200. // makes every reservation id unique across threads. A `thread_local`
  201. // generator lets two sagas on different threads mint the same id within
  202. // a millisecond, which collapses the reservation-ownership check and
  203. // allows a double-spend under concurrency.
  204. static GEN: crate::autoid::AutoId = crate::autoid::AutoId::new();
  205. Self(GEN.next())
  206. }
  207. }
  208. // ---------------------------------------------------------------------------
  209. // Book
  210. // ---------------------------------------------------------------------------
  211. /// A Book is a transfer policy scope: it gates which accounts and assets may
  212. /// participate in a transfer. It is **not** the chronological entry log (the
  213. /// transfer log plays that role), and it does **not** partition balances —
  214. /// balances are global; a Book only gates participation.
  215. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  216. pub struct Book {
  217. /// Stable identity for this book.
  218. pub id: BookId,
  219. /// Human-readable name.
  220. pub name: String,
  221. /// Participation rules for this book.
  222. pub policy: BookPolicy,
  223. }
  224. /// The participation rules for a [`Book`]. An empty field means "no restriction".
  225. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  226. pub struct BookPolicy {
  227. /// If non-empty, only these assets may appear in movements.
  228. pub allowed_assets: Vec<AssetId>,
  229. /// If non-empty, accounts with ANY of these flags may participate.
  230. pub allowed_flags: AccountFlags,
  231. /// If non-empty, these specific accounts may participate (in addition to flag matches).
  232. pub allowed_accounts: Vec<AccountId>,
  233. }
  234. /// Builder for constructing [`Book`] values.
  235. pub struct BookBuilder {
  236. book: Book,
  237. }
  238. impl BookBuilder {
  239. /// Create a new book builder with the given name.
  240. pub fn new(name: impl Into<String>) -> Self {
  241. Self {
  242. book: Book {
  243. id: BookId::generate(),
  244. name: name.into(),
  245. policy: BookPolicy {
  246. allowed_assets: Vec::new(),
  247. allowed_flags: AccountFlags::empty(),
  248. allowed_accounts: Vec::new(),
  249. },
  250. },
  251. }
  252. }
  253. /// Set the book id explicitly.
  254. pub fn id(mut self, id: BookId) -> Self {
  255. self.book.id = id;
  256. self
  257. }
  258. /// Add an allowed asset.
  259. pub fn allow_asset(mut self, asset: AssetId) -> Self {
  260. self.book.policy.allowed_assets.push(asset);
  261. self
  262. }
  263. /// Set allowed account flags — accounts with ANY of these flags may participate.
  264. pub fn allow_flags(mut self, flags: AccountFlags) -> Self {
  265. self.book.policy.allowed_flags = flags;
  266. self
  267. }
  268. /// Add a specific allowed account.
  269. pub fn allow_account(mut self, account: AccountId) -> Self {
  270. self.book.policy.allowed_accounts.push(account);
  271. self
  272. }
  273. /// Consume the builder and return the [`Book`].
  274. pub fn build(self) -> Book {
  275. self.book
  276. }
  277. }
  278. // ---------------------------------------------------------------------------
  279. // Posting
  280. // ---------------------------------------------------------------------------
  281. /// Lifecycle state of a [`Posting`].
  282. ///
  283. /// ```text
  284. /// Active ──reserve──▶ PendingInactive ──finalize──▶ Inactive (void)
  285. /// ▲ ▲ │
  286. /// │ └─── release (no-op) ┘
  287. /// └────── release ────────┘ (compensation)
  288. /// ```
  289. ///
  290. /// `reserve_postings` and `release_postings` are batch operations:
  291. /// - **reserve**: all postings must be Active, otherwise the batch fails.
  292. /// - **release**: Active is a no-op, PendingInactive reverts to Active,
  293. /// Inactive (void) fails the batch.
  294. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
  295. pub enum PostingStatus {
  296. /// Available for consumption and counted in balance.
  297. Active,
  298. /// Reserved for a transfer; not available for other consumption.
  299. /// Reverts to `Active` on compensation via `release_postings`.
  300. PendingInactive,
  301. /// Consumed by a committed transfer. Kept for audit trail (void).
  302. /// Cannot be released.
  303. Inactive,
  304. }
  305. /// A signed amount of one asset, owned by exactly one account.
  306. ///
  307. /// A positive posting is value controlled by the account; a negative posting is
  308. /// an offset position (issuance, external flow, overdraft, or system balancing).
  309. /// Negative postings are allowed on every policy except `NoOverdraft`.
  310. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  311. pub struct Posting {
  312. /// Unique identifier derived from the creating transfer.
  313. pub id: PostingId,
  314. /// The account that owns this posting.
  315. pub owner: AccountId,
  316. /// The asset this posting denominates.
  317. pub asset: AssetId,
  318. /// Signed: positive = value controlled by the account, negative = offset position.
  319. pub value: Cent,
  320. /// Lifecycle state — only `Active` postings count toward balance.
  321. pub status: PostingStatus,
  322. /// Owner token while `PendingInactive`. `Some(rid)` iff reserved by saga
  323. /// `rid`; `None` when `Active` or `Inactive`. Only the holder of a matching
  324. /// `ReservationId` may finalize or release a reserved posting.
  325. pub reservation: Option<ReservationId>,
  326. }
  327. impl Posting {
  328. /// Construct an `Active`, unreserved posting.
  329. pub fn new(id: PostingId, owner: AccountId, asset: AssetId, value: Cent) -> Self {
  330. Self {
  331. id,
  332. owner,
  333. asset,
  334. value,
  335. status: PostingStatus::Active,
  336. reservation: None,
  337. }
  338. }
  339. /// Returns `true` if this posting's status is [`PostingStatus::Active`].
  340. pub fn is_active(&self) -> bool {
  341. self.status == PostingStatus::Active
  342. }
  343. }
  344. /// A posting to be created — carries no id yet because the [`PostingId`] depends
  345. /// on the [`EnvelopeId`], which is computed during validation.
  346. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  347. pub struct NewPosting {
  348. /// The account that will own the created posting.
  349. pub owner: AccountId,
  350. /// The asset this posting denominates.
  351. pub asset: AssetId,
  352. /// Signed amount: positive = value controlled by the account, negative = offset position.
  353. pub value: Cent,
  354. /// Informational provenance — who funded this posting.
  355. pub payer: Option<AccountId>,
  356. }
  357. // ---------------------------------------------------------------------------
  358. // Transfer
  359. // ---------------------------------------------------------------------------
  360. /// Fixed-width secondary identifiers.
  361. #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
  362. pub struct UserData {
  363. /// 128-bit user-defined slot (e.g. external UUID).
  364. pub d128: u128,
  365. /// 64-bit user-defined slot (e.g. correlation id).
  366. pub d64: u64,
  367. /// 32-bit user-defined slot (e.g. category code).
  368. pub d32: u32,
  369. }
  370. /// Free-form key→value metadata.
  371. pub type Metadata = BTreeMap<String, Vec<u8>>;
  372. /// The unit of atomicity — all of its consumptions and creations apply together
  373. /// or not at all. This is the resolved, internal form produced by the saga
  374. /// pipeline from a [`Transfer`] intent.
  375. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
  376. pub struct Envelope {
  377. /// Posting ids consumed (spent) by this envelope.
  378. pub consumes: Vec<PostingId>,
  379. /// New postings created by this envelope.
  380. pub creates: Vec<NewPosting>,
  381. /// Account version pins for optimistic concurrency.
  382. pub account_snapshots: Vec<AccountSnapshotId>,
  383. /// Book this envelope belongs to.
  384. pub book: BookId,
  385. /// Fixed-width secondary identifiers.
  386. pub user_data: UserData,
  387. /// Free-form key-value metadata.
  388. pub metadata: Metadata,
  389. }
  390. impl Envelope {
  391. /// Posting ids consumed (spent) by this envelope.
  392. pub fn consumes(&self) -> &[PostingId] {
  393. &self.consumes
  394. }
  395. /// New postings created by this envelope.
  396. pub fn creates(&self) -> &[NewPosting] {
  397. &self.creates
  398. }
  399. /// Account version pins for optimistic concurrency.
  400. pub fn account_snapshots(&self) -> &[AccountSnapshotId] {
  401. &self.account_snapshots
  402. }
  403. /// Book this envelope belongs to.
  404. pub fn book(&self) -> BookId {
  405. self.book
  406. }
  407. /// Fixed-width secondary identifiers.
  408. pub fn user_data(&self) -> &UserData {
  409. &self.user_data
  410. }
  411. /// Free-form key-value metadata.
  412. pub fn metadata(&self) -> &Metadata {
  413. &self.metadata
  414. }
  415. /// Deduplicated, sorted list of accounts referenced in the created postings.
  416. pub fn referenced_accounts(&self) -> Vec<AccountId> {
  417. let mut ids: Vec<AccountId> = self.creates.iter().map(|p| p.owner).collect();
  418. ids.sort();
  419. ids.dedup();
  420. ids
  421. }
  422. /// Set account snapshots.
  423. pub fn set_account_snapshots(&mut self, snapshots: Vec<AccountSnapshotId>) {
  424. self.account_snapshots = snapshots;
  425. }
  426. }
  427. // ---------------------------------------------------------------------------
  428. // EnvelopeBuilder
  429. // ---------------------------------------------------------------------------
  430. /// Builder for constructing [`Envelope`] values.
  431. #[derive(Default)]
  432. pub struct EnvelopeBuilder {
  433. envelope: Envelope,
  434. }
  435. impl EnvelopeBuilder {
  436. /// Create an empty builder.
  437. pub fn new() -> Self {
  438. Self::default()
  439. }
  440. /// Set the posting ids to consume.
  441. pub fn consumes(mut self, ids: Vec<PostingId>) -> Self {
  442. self.envelope.consumes = ids;
  443. self
  444. }
  445. /// Set the new postings to create.
  446. pub fn creates(mut self, postings: Vec<NewPosting>) -> Self {
  447. self.envelope.creates = postings;
  448. self
  449. }
  450. /// Set the book.
  451. pub fn book(mut self, book: BookId) -> Self {
  452. self.envelope.book = book;
  453. self
  454. }
  455. /// Set the fixed-width secondary identifiers.
  456. pub fn user_data(mut self, user_data: UserData) -> Self {
  457. self.envelope.user_data = user_data;
  458. self
  459. }
  460. /// Set the account version pins.
  461. pub fn account_snapshots(mut self, snapshots: Vec<AccountSnapshotId>) -> Self {
  462. self.envelope.account_snapshots = snapshots;
  463. self
  464. }
  465. /// Set the free-form metadata.
  466. pub fn metadata(mut self, metadata: Metadata) -> Self {
  467. self.envelope.metadata = metadata;
  468. self
  469. }
  470. /// Consume the builder and return the [`Envelope`].
  471. pub fn build(self) -> Envelope {
  472. self.envelope
  473. }
  474. }
  475. // ---------------------------------------------------------------------------
  476. // Account
  477. // ---------------------------------------------------------------------------
  478. /// Controls how much an account can spend beyond its posting-backed balance.
  479. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  480. pub enum AccountPolicy {
  481. /// Balance must stay >= 0.
  482. NoOverdraft,
  483. /// Balance must stay >= `floor` (floor < 0).
  484. CappedOverdraft {
  485. /// Minimum allowed balance (must be negative).
  486. floor: Cent,
  487. },
  488. /// No floor — the account can go arbitrarily negative.
  489. UncappedOverdraft,
  490. /// Fees, settlement, market-making, minting. No balance constraints.
  491. SystemAccount,
  492. /// Boundary account representing value entering/leaving the ledger; holds
  493. /// the offset (negative) side of deposits.
  494. ExternalAccount,
  495. }
  496. bitflags::bitflags! {
  497. /// Lifecycle and user-defined flags for an [`Account`].
  498. ///
  499. /// Bits 0–7 are reserved for system flags. Bits 8–31 are available for
  500. /// user-defined flags, which can be used with [`BookPolicy::allowed_flags`]
  501. /// to scope which accounts may participate in a book.
  502. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
  503. pub struct AccountFlags: u32 {
  504. /// Account may not be the source or destination of any transfer.
  505. const FROZEN = 1 << 0;
  506. /// Terminal — no further activity.
  507. const CLOSED = 1 << 1;
  508. // Bits 2–7: reserved for future system flags.
  509. // Bits 8–31: user-defined.
  510. /// User-defined flag 0.
  511. const USER_0 = 1 << 8;
  512. /// User-defined flag 1.
  513. const USER_1 = 1 << 9;
  514. /// User-defined flag 2.
  515. const USER_2 = 1 << 10;
  516. /// User-defined flag 3.
  517. const USER_3 = 1 << 11;
  518. /// User-defined flag 4.
  519. const USER_4 = 1 << 12;
  520. /// User-defined flag 5.
  521. const USER_5 = 1 << 13;
  522. /// User-defined flag 6.
  523. const USER_6 = 1 << 14;
  524. /// User-defined flag 7.
  525. const USER_7 = 1 << 15;
  526. }
  527. }
  528. /// A registered entity that must exist before it can transact.
  529. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  530. pub struct Account {
  531. /// Stable identity for this account.
  532. pub id: AccountId,
  533. /// Monotonically increasing version, starts at 1 on creation.
  534. pub version: u64,
  535. /// Overdraft / balance policy.
  536. pub policy: AccountPolicy,
  537. /// Lifecycle flags (frozen, closed).
  538. pub flags: AccountFlags,
  539. /// Book this entity belongs to.
  540. pub book: BookId,
  541. /// Fixed-width secondary identifiers.
  542. pub user_data: UserData,
  543. /// Free-form key-value metadata.
  544. pub metadata: Metadata,
  545. }
  546. impl Account {
  547. /// Create a version-1 account with the given policy: no flags, the default
  548. /// book, and empty user data / metadata. Convenience for the common case —
  549. /// set the other fields explicitly when you need them.
  550. pub fn new(id: AccountId, policy: AccountPolicy) -> Self {
  551. Self {
  552. id,
  553. version: 1,
  554. policy,
  555. flags: AccountFlags::empty(),
  556. book: DEFAULT_BOOK,
  557. user_data: UserData::default(),
  558. metadata: Metadata::new(),
  559. }
  560. }
  561. /// Returns `true` if the account has the `FROZEN` flag set.
  562. pub fn is_frozen(&self) -> bool {
  563. self.flags.contains(AccountFlags::FROZEN)
  564. }
  565. /// Returns `true` if the account has the `CLOSED` flag set.
  566. pub fn is_closed(&self) -> bool {
  567. self.flags.contains(AccountFlags::CLOSED)
  568. }
  569. }
  570. // ---------------------------------------------------------------------------
  571. // Receipt
  572. // ---------------------------------------------------------------------------
  573. /// Confirmation of a committed transfer, carrying its content-addressed id.
  574. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  575. pub struct Receipt {
  576. /// Content-addressed id of the committed transfer.
  577. pub transfer_id: EnvelopeId,
  578. }
  579. // ---------------------------------------------------------------------------
  580. // Transfer — intent-based API
  581. // ---------------------------------------------------------------------------
  582. /// A single movement within a transfer: move value from one account to another.
  583. ///
  584. /// Every operation (pay, deposit, withdraw) is expressed as one or more
  585. /// movements. The resolve step aggregates net debits per account and selects
  586. /// postings only for accounts with a positive net debit.
  587. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  588. pub struct Movement {
  589. /// Account being debited.
  590. pub from: AccountId,
  591. /// Account being credited.
  592. pub to: AccountId,
  593. /// Asset to transfer.
  594. pub asset: AssetId,
  595. /// Amount to transfer (may be negative for offset postings).
  596. pub amount: Cent,
  597. }
  598. /// A transfer intent — one or more movements to execute atomically.
  599. ///
  600. /// The saga pipeline resolves movements into concrete postings ([`Envelope`])
  601. /// during execution. Callers express *what* should happen, not *which postings*
  602. /// to consume.
  603. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
  604. pub struct Transfer {
  605. /// Movements to execute atomically.
  606. pub movements: Vec<Movement>,
  607. /// Book this entity belongs to.
  608. pub book: BookId,
  609. /// Fixed-width secondary identifiers.
  610. pub user_data: UserData,
  611. /// Free-form key-value metadata.
  612. pub metadata: Metadata,
  613. }
  614. /// Builder for constructing [`Transfer`] values.
  615. #[derive(Default)]
  616. pub struct TransferBuilder {
  617. transfer: Transfer,
  618. }
  619. impl TransferBuilder {
  620. /// Create an empty builder.
  621. pub fn new() -> Self {
  622. Self::default()
  623. }
  624. /// Add a raw movement.
  625. pub fn movement(
  626. mut self,
  627. from: AccountId,
  628. to: AccountId,
  629. asset: AssetId,
  630. amount: Cent,
  631. ) -> Self {
  632. self.transfer.movements.push(Movement {
  633. from,
  634. to,
  635. asset,
  636. amount,
  637. });
  638. self
  639. }
  640. /// Add a pay movement: transfer value between two accounts.
  641. pub fn pay(self, from: AccountId, to: AccountId, asset: AssetId, amount: Cent) -> Self {
  642. self.movement(from, to, asset, amount)
  643. }
  644. /// Add a deposit: creates an offset posting on the external account and
  645. /// credits the target account. Pushes two movements whose net debit on the
  646. /// external account is zero.
  647. pub fn deposit(
  648. self,
  649. to: AccountId,
  650. asset: AssetId,
  651. amount: Cent,
  652. external: AccountId,
  653. ) -> Result<Self, OverflowError> {
  654. let neg = amount.checked_neg()?;
  655. Ok(self
  656. .movement(external, external, asset, neg)
  657. .movement(external, to, asset, amount))
  658. }
  659. /// Add a withdrawal: move value from an account to an external destination.
  660. pub fn withdraw(
  661. self,
  662. from: AccountId,
  663. asset: AssetId,
  664. amount: Cent,
  665. external: AccountId,
  666. ) -> Self {
  667. self.movement(from, external, asset, amount)
  668. }
  669. /// Set the book.
  670. pub fn book(mut self, book: BookId) -> Self {
  671. self.transfer.book = book;
  672. self
  673. }
  674. /// Set the fixed-width secondary identifiers.
  675. pub fn user_data(mut self, user_data: UserData) -> Self {
  676. self.transfer.user_data = user_data;
  677. self
  678. }
  679. /// Set the free-form metadata.
  680. pub fn metadata(mut self, metadata: Metadata) -> Self {
  681. self.transfer.metadata = metadata;
  682. self
  683. }
  684. /// Consume the builder and return the [`Transfer`].
  685. pub fn build(self) -> Transfer {
  686. self.transfer
  687. }
  688. }
  689. // ---------------------------------------------------------------------------
  690. // ToBytes implementations
  691. // ---------------------------------------------------------------------------
  692. impl ToBytes for AccountId {
  693. fn to_bytes(&self) -> Vec<u8> {
  694. self.0.to_be_bytes().to_vec()
  695. }
  696. }
  697. impl ToBytes for AccountSnapshotId {
  698. fn to_bytes(&self) -> Vec<u8> {
  699. let mut buf = Vec::with_capacity(40);
  700. buf.extend_from_slice(&self.account.0.to_be_bytes());
  701. buf.extend_from_slice(&self.snapshot_id);
  702. buf
  703. }
  704. }
  705. impl ToBytes for AssetId {
  706. fn to_bytes(&self) -> Vec<u8> {
  707. self.0.to_be_bytes().to_vec()
  708. }
  709. }
  710. impl ToBytes for EnvelopeId {
  711. fn to_bytes(&self) -> Vec<u8> {
  712. self.0.to_vec()
  713. }
  714. }
  715. impl ToBytes for PostingId {
  716. fn to_bytes(&self) -> Vec<u8> {
  717. let mut buf = Vec::with_capacity(34);
  718. buf.extend_from_slice(&self.transfer.0);
  719. write_u16(&mut buf, self.index);
  720. buf
  721. }
  722. }
  723. impl ToBytes for UserData {
  724. fn to_bytes(&self) -> Vec<u8> {
  725. let mut buf = Vec::with_capacity(28);
  726. write_u128(&mut buf, self.d128);
  727. write_u64(&mut buf, self.d64);
  728. write_u32(&mut buf, self.d32);
  729. buf
  730. }
  731. }
  732. impl ToBytes for AccountPolicy {
  733. fn to_bytes(&self) -> Vec<u8> {
  734. let mut buf = Vec::with_capacity(9);
  735. match self {
  736. Self::NoOverdraft => buf.push(0),
  737. Self::CappedOverdraft { floor } => {
  738. buf.push(1);
  739. buf.extend(floor.to_bytes());
  740. }
  741. Self::UncappedOverdraft => buf.push(2),
  742. Self::SystemAccount => buf.push(3),
  743. Self::ExternalAccount => buf.push(4),
  744. }
  745. buf
  746. }
  747. }
  748. impl ToBytes for AccountFlags {
  749. fn to_bytes(&self) -> Vec<u8> {
  750. self.bits().to_be_bytes().to_vec()
  751. }
  752. }
  753. impl ToBytes for BookId {
  754. fn to_bytes(&self) -> Vec<u8> {
  755. self.0.to_be_bytes().to_vec()
  756. }
  757. }
  758. impl ToBytes for NewPosting {
  759. fn to_bytes(&self) -> Vec<u8> {
  760. let mut buf = Vec::new();
  761. buf.extend(self.owner.to_bytes());
  762. buf.extend_from_slice(&self.asset.0.to_be_bytes());
  763. buf.extend(self.value.to_bytes());
  764. match &self.payer {
  765. Some(p) => {
  766. buf.push(1);
  767. buf.extend(p.to_bytes());
  768. }
  769. None => buf.push(0),
  770. }
  771. buf
  772. }
  773. }
  774. impl ToBytes for Posting {
  775. fn to_bytes(&self) -> Vec<u8> {
  776. let mut buf = Vec::new();
  777. buf.extend(self.id.to_bytes());
  778. buf.extend(self.owner.to_bytes());
  779. buf.extend_from_slice(&self.asset.0.to_be_bytes());
  780. buf.extend(self.value.to_bytes());
  781. buf.push(match self.status {
  782. PostingStatus::Active => 0,
  783. PostingStatus::PendingInactive => 1,
  784. PostingStatus::Inactive => 2,
  785. });
  786. buf
  787. }
  788. }
  789. impl ToBytes for Envelope {
  790. fn to_bytes(&self) -> Vec<u8> {
  791. let mut buf = Vec::new();
  792. buf.push(CANONICAL_VERSION);
  793. write_u32(&mut buf, self.consumes.len() as u32);
  794. for pid in &self.consumes {
  795. buf.extend(pid.to_bytes());
  796. }
  797. write_u32(&mut buf, self.creates.len() as u32);
  798. for np in &self.creates {
  799. buf.extend(np.to_bytes());
  800. }
  801. write_u32(&mut buf, self.account_snapshots.len() as u32);
  802. for snap in &self.account_snapshots {
  803. buf.extend(snap.to_bytes());
  804. }
  805. buf.extend(self.book.to_bytes());
  806. buf.extend(self.user_data.to_bytes());
  807. write_u32(&mut buf, self.metadata.len() as u32);
  808. for (key, value) in &self.metadata {
  809. let key_bytes = key.as_bytes();
  810. write_u32(&mut buf, key_bytes.len() as u32);
  811. buf.extend_from_slice(key_bytes);
  812. write_u32(&mut buf, value.len() as u32);
  813. buf.extend_from_slice(value);
  814. }
  815. buf
  816. }
  817. }
  818. impl ToBytes for Account {
  819. fn to_bytes(&self) -> Vec<u8> {
  820. let mut buf = Vec::new();
  821. buf.push(CANONICAL_VERSION);
  822. buf.extend(self.id.to_bytes());
  823. write_u64(&mut buf, self.version);
  824. buf.extend(self.policy.to_bytes());
  825. buf.extend(self.flags.to_bytes());
  826. buf.extend(self.book.to_bytes());
  827. buf.extend(self.user_data.to_bytes());
  828. write_u32(&mut buf, self.metadata.len() as u32);
  829. for (key, value) in &self.metadata {
  830. let key_bytes = key.as_bytes();
  831. write_u32(&mut buf, key_bytes.len() as u32);
  832. buf.extend_from_slice(key_bytes);
  833. write_u32(&mut buf, value.len() as u32);
  834. buf.extend_from_slice(value);
  835. }
  836. buf
  837. }
  838. }
  839. impl ToBytes for Receipt {
  840. fn to_bytes(&self) -> Vec<u8> {
  841. self.transfer_id.0.to_vec()
  842. }
  843. }