lib.rs 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198
  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. pub const CANONICAL_VERSION: u8 = 1;
  25. /// Append a `u16` in big-endian to `buf`.
  26. pub fn write_u16(buf: &mut Vec<u8>, v: u16) {
  27. buf.extend_from_slice(&v.to_be_bytes());
  28. }
  29. /// Append a `u32` in big-endian to `buf`.
  30. pub fn write_u32(buf: &mut Vec<u8>, v: u32) {
  31. buf.extend_from_slice(&v.to_be_bytes());
  32. }
  33. /// Append a `u64` in big-endian to `buf`.
  34. pub fn write_u64(buf: &mut Vec<u8>, v: u64) {
  35. buf.extend_from_slice(&v.to_be_bytes());
  36. }
  37. /// Append an `i64` in big-endian to `buf`.
  38. pub fn write_i64(buf: &mut Vec<u8>, v: i64) {
  39. buf.extend_from_slice(&v.to_be_bytes());
  40. }
  41. /// Append a `u128` in big-endian to `buf`.
  42. pub fn write_u128(buf: &mut Vec<u8>, v: u128) {
  43. buf.extend_from_slice(&v.to_be_bytes());
  44. }
  45. // ---------------------------------------------------------------------------
  46. // Identifiers
  47. // ---------------------------------------------------------------------------
  48. /// Stable account identity. Used in all public APIs.
  49. #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
  50. pub struct AccountId(pub i64);
  51. /// Pairs an [`AccountId`] with a snapshot hash — the double-SHA256 of the
  52. /// account's state at a point in time. Stored on [`Transfer`] to record which
  53. /// account versions a transfer was executed against. Internal type — the
  54. /// public API uses [`AccountId`].
  55. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  56. pub struct AccountSnapshotId {
  57. /// The account this snapshot belongs to.
  58. pub account: AccountId,
  59. /// Double-SHA256 of the account's state at the time of the snapshot.
  60. pub snapshot_id: [u8; 32],
  61. }
  62. /// Identifies an asset (USD, EUR, BTC, …). Conservation is enforced per asset,
  63. /// so each asset is an independent conservation boundary.
  64. #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
  65. pub struct AssetId(pub u32);
  66. /// Content-addressed transfer identifier — the double-SHA256 of the canonical
  67. /// serialization. This makes the id both the idempotency key and the
  68. /// tamper-evidence artifact.
  69. #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
  70. pub struct EnvelopeId(pub [u8; 32]);
  71. /// Uniquely identifies a posting within the ledger. The `(transfer, index)` pair
  72. /// ties every posting back to the transfer that created it, which is the basis
  73. /// of the provenance graph.
  74. #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
  75. pub struct PostingId {
  76. /// The transfer that created this posting.
  77. pub transfer: EnvelopeId,
  78. /// Zero-based position within the transfer's created postings.
  79. pub index: u16,
  80. }
  81. // ---------------------------------------------------------------------------
  82. // Cent — low-level stored monetary amount
  83. // ---------------------------------------------------------------------------
  84. /// A monetary amount in the smallest unit (e.g. cents for USD).
  85. ///
  86. /// Wraps `i64` with a private field so that monetary values are never confused
  87. /// with plain integers. Used everywhere a monetary amount is stored or compared.
  88. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
  89. pub struct Cent(i64);
  90. /// Returned when a [`Cent`] arithmetic operation would overflow or underflow.
  91. #[derive(Debug, Clone, Copy, PartialEq, Eq)]
  92. pub struct OverflowError;
  93. impl fmt::Display for OverflowError {
  94. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  95. write!(f, "monetary amount overflow")
  96. }
  97. }
  98. impl std::error::Error for OverflowError {}
  99. impl Cent {
  100. /// The zero amount.
  101. pub const ZERO: Cent = Cent(0);
  102. /// Returns the underlying `i64` value.
  103. pub fn value(self) -> i64 {
  104. self.0
  105. }
  106. /// Returns `true` if the amount is strictly positive.
  107. pub fn is_positive(self) -> bool {
  108. self.0 > 0
  109. }
  110. /// Returns `true` if the amount is strictly negative.
  111. pub fn is_negative(self) -> bool {
  112. self.0 < 0
  113. }
  114. /// Returns `true` if the amount is zero.
  115. pub fn is_zero(self) -> bool {
  116. self.0 == 0
  117. }
  118. /// Checked addition, returning [`OverflowError`] on overflow.
  119. pub fn checked_add(self, rhs: Self) -> Result<Self, OverflowError> {
  120. self.0.checked_add(rhs.0).map(Cent).ok_or(OverflowError)
  121. }
  122. /// Checked subtraction, returning [`OverflowError`] on underflow.
  123. pub fn checked_sub(self, rhs: Self) -> Result<Self, OverflowError> {
  124. self.0.checked_sub(rhs.0).map(Cent).ok_or(OverflowError)
  125. }
  126. /// Checked negation, returning [`OverflowError`] if `self == i64::MIN`.
  127. pub fn checked_neg(self) -> Result<Self, OverflowError> {
  128. self.0.checked_neg().map(Cent).ok_or(OverflowError)
  129. }
  130. /// Sum an iterator of `Cent` values with overflow checking.
  131. pub fn checked_sum(iter: impl IntoIterator<Item = Self>) -> Result<Self, OverflowError> {
  132. let mut sum = Cent::ZERO;
  133. for x in iter {
  134. sum = sum.checked_add(x)?;
  135. }
  136. Ok(sum)
  137. }
  138. }
  139. impl From<i64> for Cent {
  140. fn from(v: i64) -> Self {
  141. Self(v)
  142. }
  143. }
  144. impl From<i32> for Cent {
  145. fn from(v: i32) -> Self {
  146. Self(v as i64)
  147. }
  148. }
  149. impl From<u32> for Cent {
  150. fn from(v: u32) -> Self {
  151. Self(v as i64)
  152. }
  153. }
  154. impl From<u8> for Cent {
  155. fn from(v: u8) -> Self {
  156. Self(v as i64)
  157. }
  158. }
  159. impl From<i8> for Cent {
  160. fn from(v: i8) -> Self {
  161. Self(v as i64)
  162. }
  163. }
  164. impl fmt::Debug for Cent {
  165. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  166. write!(f, "Cent({})", self.0)
  167. }
  168. }
  169. impl fmt::Display for Cent {
  170. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  171. write!(f, "{}", self.0)
  172. }
  173. }
  174. impl ToBytes for Cent {
  175. fn to_bytes(&self) -> Vec<u8> {
  176. self.0.to_be_bytes().to_vec()
  177. }
  178. }
  179. // ---------------------------------------------------------------------------
  180. // Amount — human-friendly parser/formatter (not stored)
  181. // ---------------------------------------------------------------------------
  182. /// Parses and formats human-readable amounts with a fixed number of decimal
  183. /// places. NOT stored anywhere — used only to convert between strings and
  184. /// [`Cent`] values.
  185. pub struct Amount {
  186. decimals: u8,
  187. }
  188. /// Error returned when parsing an amount string fails.
  189. #[derive(Debug, Clone, PartialEq, Eq)]
  190. pub enum ParseAmountError {
  191. /// The input string is not a valid number.
  192. InvalidFormat(String),
  193. /// Too many decimal places for the configured precision.
  194. TooManyDecimals {
  195. /// Maximum allowed decimal places.
  196. max: u8,
  197. /// Number of decimal places found in the input.
  198. found: usize,
  199. },
  200. }
  201. impl fmt::Display for ParseAmountError {
  202. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  203. match self {
  204. Self::InvalidFormat(s) => write!(f, "invalid amount format: {s}"),
  205. Self::TooManyDecimals { max, found } => {
  206. write!(f, "too many decimals: max {max}, found {found}")
  207. }
  208. }
  209. }
  210. }
  211. impl std::error::Error for ParseAmountError {}
  212. impl Amount {
  213. /// Create an `Amount` formatter with the given number of decimal places.
  214. pub fn new(decimals: u8) -> Self {
  215. Self { decimals }
  216. }
  217. /// Parses a decimal string into a [`Cent`] value.
  218. pub fn parse(&self, s: &str) -> Result<Cent, ParseAmountError> {
  219. let s = s.trim();
  220. let (negative, s) = if let Some(rest) = s.strip_prefix('-') {
  221. (true, rest)
  222. } else {
  223. (false, s)
  224. };
  225. let (whole_str, frac_str) = if let Some((w, f)) = s.split_once('.') {
  226. (w, f)
  227. } else {
  228. (s, "")
  229. };
  230. if whole_str.is_empty() && frac_str.is_empty() {
  231. return Err(ParseAmountError::InvalidFormat(s.to_string()));
  232. }
  233. let whole: i64 = if whole_str.is_empty() {
  234. 0
  235. } else {
  236. whole_str
  237. .parse()
  238. .map_err(|_| ParseAmountError::InvalidFormat(s.to_string()))?
  239. };
  240. if frac_str.len() > self.decimals as usize {
  241. return Err(ParseAmountError::TooManyDecimals {
  242. max: self.decimals,
  243. found: frac_str.len(),
  244. });
  245. }
  246. if !frac_str.is_empty() && !frac_str.chars().all(|c| c.is_ascii_digit()) {
  247. return Err(ParseAmountError::InvalidFormat(s.to_string()));
  248. }
  249. let frac: i64 = if frac_str.is_empty() {
  250. 0
  251. } else {
  252. let padded = format!("{:0<width$}", frac_str, width = self.decimals as usize);
  253. padded
  254. .parse()
  255. .map_err(|_| ParseAmountError::InvalidFormat(s.to_string()))?
  256. };
  257. let multiplier = 10i64.pow(self.decimals as u32);
  258. let value = whole
  259. .checked_mul(multiplier)
  260. .and_then(|v| v.checked_add(frac))
  261. .ok_or_else(|| ParseAmountError::InvalidFormat(s.to_string()))?;
  262. Ok(Cent::from(if negative {
  263. value
  264. .checked_neg()
  265. .ok_or_else(|| ParseAmountError::InvalidFormat(s.to_string()))?
  266. } else {
  267. value
  268. }))
  269. }
  270. /// Formats a [`Cent`] value as a decimal string.
  271. pub fn format(&self, cent: Cent) -> String {
  272. if self.decimals == 0 {
  273. return cent.value().to_string();
  274. }
  275. let value = cent.value();
  276. let negative = value < 0;
  277. let abs = value.unsigned_abs();
  278. let multiplier = 10u64.pow(self.decimals as u32);
  279. let whole = abs / multiplier;
  280. let frac = abs % multiplier;
  281. let sign = if negative { "-" } else { "" };
  282. format!(
  283. "{sign}{whole}.{frac:0>width$}",
  284. width = self.decimals as usize
  285. )
  286. }
  287. }
  288. // ---------------------------------------------------------------------------
  289. // Debug / Display impls for identifiers
  290. // ---------------------------------------------------------------------------
  291. impl fmt::Debug for AccountId {
  292. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  293. write!(f, "AccountId({})", self.0)
  294. }
  295. }
  296. impl fmt::Debug for AssetId {
  297. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  298. write!(f, "AssetId({:#010x})", self.0)
  299. }
  300. }
  301. impl fmt::Debug for EnvelopeId {
  302. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  303. write!(f, "EnvelopeId({})", hex(&self.0))
  304. }
  305. }
  306. impl fmt::Debug for PostingId {
  307. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  308. f.debug_struct("PostingId")
  309. .field("transfer", &self.transfer)
  310. .field("index", &self.index)
  311. .finish()
  312. }
  313. }
  314. fn hex(bytes: &[u8]) -> String {
  315. bytes.iter().map(|b| format!("{b:02x}")).collect()
  316. }
  317. // ---------------------------------------------------------------------------
  318. // Identifier constructors
  319. // ---------------------------------------------------------------------------
  320. impl Default for AccountId {
  321. fn default() -> Self {
  322. thread_local! {
  323. static GEN: crate::autoid::AutoId = crate::autoid::AutoId::new();
  324. }
  325. GEN.with(|g| Self(g.next()))
  326. }
  327. }
  328. impl AccountId {
  329. /// Create an `AccountId` from an `i64`.
  330. pub const fn new(id: i64) -> Self {
  331. Self(id)
  332. }
  333. }
  334. impl From<AccountSnapshotId> for AccountId {
  335. fn from(snap: AccountSnapshotId) -> Self {
  336. snap.account
  337. }
  338. }
  339. impl AssetId {
  340. /// Create an `AssetId` from a `u32`.
  341. pub const fn new(id: u32) -> Self {
  342. Self(id)
  343. }
  344. }
  345. /// Identifies a book — a named scope for transfers.
  346. #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
  347. pub struct BookId(pub i64);
  348. impl fmt::Debug for BookId {
  349. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  350. write!(f, "BookId({})", self.0)
  351. }
  352. }
  353. /// The implicit book used when a transfer does not name one. Fixed so that two
  354. /// otherwise-identical transfers hash to the same [`EnvelopeId`] — a random
  355. /// default would break content-addressed idempotency.
  356. pub const DEFAULT_BOOK: BookId = BookId(0);
  357. impl Default for BookId {
  358. /// Deterministic: returns [`DEFAULT_BOOK`]. Use [`BookId::generate`] to mint
  359. /// a fresh unique id for a real book.
  360. fn default() -> Self {
  361. DEFAULT_BOOK
  362. }
  363. }
  364. impl BookId {
  365. /// Create a `BookId` from an `i64`.
  366. pub const fn new(id: i64) -> Self {
  367. Self(id)
  368. }
  369. /// Mint a fresh, process-unique book id. Unlike [`Default`], this is not
  370. /// stable across calls — use it when creating a new [`Book`], never for the
  371. /// implicit book of a transfer.
  372. pub fn generate() -> Self {
  373. thread_local! {
  374. static GEN: crate::autoid::AutoId = crate::autoid::AutoId::new();
  375. }
  376. GEN.with(|g| Self(g.next()))
  377. }
  378. }
  379. /// Identifies a reservation — the owner token stamped on a posting while it is
  380. /// `PendingInactive`, so only the saga that reserved it may finalize or release it.
  381. #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
  382. pub struct ReservationId(pub i64);
  383. impl fmt::Debug for ReservationId {
  384. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  385. write!(f, "ReservationId({})", self.0)
  386. }
  387. }
  388. impl ReservationId {
  389. /// Create a `ReservationId` from an `i64`.
  390. pub const fn new(id: i64) -> Self {
  391. Self(id)
  392. }
  393. }
  394. impl Default for ReservationId {
  395. fn default() -> Self {
  396. thread_local! {
  397. static GEN: crate::autoid::AutoId = crate::autoid::AutoId::new();
  398. }
  399. GEN.with(|g| Self(g.next()))
  400. }
  401. }
  402. // ---------------------------------------------------------------------------
  403. // Book
  404. // ---------------------------------------------------------------------------
  405. /// A Book is a transfer policy scope: it gates which accounts and assets may
  406. /// participate in a transfer. It is **not** the chronological entry log (the
  407. /// transfer log plays that role), and it does **not** partition balances —
  408. /// balances are global; a Book only gates participation.
  409. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  410. pub struct Book {
  411. /// Stable identity for this book.
  412. pub id: BookId,
  413. /// Human-readable name.
  414. pub name: String,
  415. /// Participation rules for this book.
  416. pub policy: BookPolicy,
  417. }
  418. /// The participation rules for a [`Book`]. An empty field means "no restriction".
  419. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  420. pub struct BookPolicy {
  421. /// If non-empty, only these assets may appear in movements.
  422. pub allowed_assets: Vec<AssetId>,
  423. /// If non-empty, accounts with ANY of these flags may participate.
  424. pub allowed_flags: AccountFlags,
  425. /// If non-empty, these specific accounts may participate (in addition to flag matches).
  426. pub allowed_accounts: Vec<AccountId>,
  427. }
  428. /// Builder for constructing [`Book`] values.
  429. pub struct BookBuilder {
  430. book: Book,
  431. }
  432. impl BookBuilder {
  433. /// Create a new book builder with the given name.
  434. pub fn new(name: impl Into<String>) -> Self {
  435. Self {
  436. book: Book {
  437. id: BookId::generate(),
  438. name: name.into(),
  439. policy: BookPolicy {
  440. allowed_assets: Vec::new(),
  441. allowed_flags: AccountFlags::empty(),
  442. allowed_accounts: Vec::new(),
  443. },
  444. },
  445. }
  446. }
  447. /// Set the book id explicitly.
  448. pub fn id(mut self, id: BookId) -> Self {
  449. self.book.id = id;
  450. self
  451. }
  452. /// Add an allowed asset.
  453. pub fn allow_asset(mut self, asset: AssetId) -> Self {
  454. self.book.policy.allowed_assets.push(asset);
  455. self
  456. }
  457. /// Set allowed account flags — accounts with ANY of these flags may participate.
  458. pub fn allow_flags(mut self, flags: AccountFlags) -> Self {
  459. self.book.policy.allowed_flags = flags;
  460. self
  461. }
  462. /// Add a specific allowed account.
  463. pub fn allow_account(mut self, account: AccountId) -> Self {
  464. self.book.policy.allowed_accounts.push(account);
  465. self
  466. }
  467. /// Consume the builder and return the [`Book`].
  468. pub fn build(self) -> Book {
  469. self.book
  470. }
  471. }
  472. // ---------------------------------------------------------------------------
  473. // Posting
  474. // ---------------------------------------------------------------------------
  475. /// Lifecycle state of a [`Posting`].
  476. ///
  477. /// ```text
  478. /// Active ──reserve──▶ PendingInactive ──finalize──▶ Inactive (void)
  479. /// ▲ ▲ │
  480. /// │ └─── release (no-op) ┘
  481. /// └────── release ────────┘ (compensation)
  482. /// ```
  483. ///
  484. /// `reserve_postings` and `release_postings` are batch operations:
  485. /// - **reserve**: all postings must be Active, otherwise the batch fails.
  486. /// - **release**: Active is a no-op, PendingInactive reverts to Active,
  487. /// Inactive (void) fails the batch.
  488. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
  489. pub enum PostingStatus {
  490. /// Available for consumption and counted in balance.
  491. Active,
  492. /// Reserved for a transfer; not available for other consumption.
  493. /// Reverts to `Active` on compensation via `release_postings`.
  494. PendingInactive,
  495. /// Consumed by a committed transfer. Kept for audit trail (void).
  496. /// Cannot be released.
  497. Inactive,
  498. }
  499. /// A signed amount of one asset, owned by exactly one account.
  500. ///
  501. /// A positive posting is value controlled by the account; a negative posting is
  502. /// an offset position (issuance, external flow, overdraft, or system balancing).
  503. /// Negative postings are allowed on every policy except `NoOverdraft`.
  504. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  505. pub struct Posting {
  506. /// Unique identifier derived from the creating transfer.
  507. pub id: PostingId,
  508. /// The account that owns this posting.
  509. pub owner: AccountId,
  510. /// The asset this posting denominates.
  511. pub asset: AssetId,
  512. /// Signed: positive = value controlled by the account, negative = offset position.
  513. pub value: Cent,
  514. /// Lifecycle state — only `Active` postings count toward balance.
  515. pub status: PostingStatus,
  516. /// Owner token while `PendingInactive`. `Some(rid)` iff reserved by saga
  517. /// `rid`; `None` when `Active` or `Inactive`. Only the holder of a matching
  518. /// `ReservationId` may finalize or release a reserved posting.
  519. pub reservation: Option<ReservationId>,
  520. }
  521. impl Posting {
  522. /// Construct an `Active`, unreserved posting.
  523. pub fn new(id: PostingId, owner: AccountId, asset: AssetId, value: Cent) -> Self {
  524. Self {
  525. id,
  526. owner,
  527. asset,
  528. value,
  529. status: PostingStatus::Active,
  530. reservation: None,
  531. }
  532. }
  533. /// Returns `true` if this posting's status is [`PostingStatus::Active`].
  534. pub fn is_active(&self) -> bool {
  535. self.status == PostingStatus::Active
  536. }
  537. }
  538. /// A posting to be created — carries no id yet because the [`PostingId`] depends
  539. /// on the [`EnvelopeId`], which is computed during validation.
  540. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  541. pub struct NewPosting {
  542. /// The account that will own the created posting.
  543. pub owner: AccountId,
  544. /// The asset this posting denominates.
  545. pub asset: AssetId,
  546. /// Signed amount: positive = value controlled by the account, negative = offset position.
  547. pub value: Cent,
  548. /// Informational provenance — who funded this posting.
  549. pub payer: Option<AccountId>,
  550. }
  551. // ---------------------------------------------------------------------------
  552. // Transfer
  553. // ---------------------------------------------------------------------------
  554. /// Fixed-width secondary identifiers.
  555. #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
  556. pub struct UserData {
  557. /// 128-bit user-defined slot (e.g. external UUID).
  558. pub d128: u128,
  559. /// 64-bit user-defined slot (e.g. correlation id).
  560. pub d64: u64,
  561. /// 32-bit user-defined slot (e.g. category code).
  562. pub d32: u32,
  563. }
  564. /// Free-form key→value metadata.
  565. pub type Metadata = BTreeMap<String, Vec<u8>>;
  566. /// The unit of atomicity — all of its consumptions and creations apply together
  567. /// or not at all. This is the resolved, internal form produced by the saga
  568. /// pipeline from a [`Transfer`] intent.
  569. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
  570. pub struct Envelope {
  571. /// Posting ids consumed (spent) by this envelope.
  572. pub consumes: Vec<PostingId>,
  573. /// New postings created by this envelope.
  574. pub creates: Vec<NewPosting>,
  575. /// Account version pins for optimistic concurrency.
  576. pub account_snapshots: Vec<AccountSnapshotId>,
  577. /// Book this envelope belongs to.
  578. pub book: BookId,
  579. /// Fixed-width secondary identifiers.
  580. pub user_data: UserData,
  581. /// Free-form key-value metadata.
  582. pub metadata: Metadata,
  583. }
  584. impl Envelope {
  585. /// Posting ids consumed (spent) by this envelope.
  586. pub fn consumes(&self) -> &[PostingId] {
  587. &self.consumes
  588. }
  589. /// New postings created by this envelope.
  590. pub fn creates(&self) -> &[NewPosting] {
  591. &self.creates
  592. }
  593. /// Account version pins for optimistic concurrency.
  594. pub fn account_snapshots(&self) -> &[AccountSnapshotId] {
  595. &self.account_snapshots
  596. }
  597. /// Book this envelope belongs to.
  598. pub fn book(&self) -> BookId {
  599. self.book
  600. }
  601. /// Fixed-width secondary identifiers.
  602. pub fn user_data(&self) -> &UserData {
  603. &self.user_data
  604. }
  605. /// Free-form key-value metadata.
  606. pub fn metadata(&self) -> &Metadata {
  607. &self.metadata
  608. }
  609. /// Deduplicated, sorted list of accounts referenced in the created postings.
  610. pub fn referenced_accounts(&self) -> Vec<AccountId> {
  611. let mut ids: Vec<AccountId> = self.creates.iter().map(|p| p.owner).collect();
  612. ids.sort();
  613. ids.dedup();
  614. ids
  615. }
  616. /// Set account snapshots.
  617. pub fn set_account_snapshots(&mut self, snapshots: Vec<AccountSnapshotId>) {
  618. self.account_snapshots = snapshots;
  619. }
  620. }
  621. // ---------------------------------------------------------------------------
  622. // EnvelopeBuilder
  623. // ---------------------------------------------------------------------------
  624. /// Builder for constructing [`Envelope`] values.
  625. #[derive(Default)]
  626. pub struct EnvelopeBuilder {
  627. envelope: Envelope,
  628. }
  629. impl EnvelopeBuilder {
  630. /// Create an empty builder.
  631. pub fn new() -> Self {
  632. Self::default()
  633. }
  634. /// Set the posting ids to consume.
  635. pub fn consumes(mut self, ids: Vec<PostingId>) -> Self {
  636. self.envelope.consumes = ids;
  637. self
  638. }
  639. /// Set the new postings to create.
  640. pub fn creates(mut self, postings: Vec<NewPosting>) -> Self {
  641. self.envelope.creates = postings;
  642. self
  643. }
  644. /// Set the book.
  645. pub fn book(mut self, book: BookId) -> Self {
  646. self.envelope.book = book;
  647. self
  648. }
  649. /// Set the fixed-width secondary identifiers.
  650. pub fn user_data(mut self, user_data: UserData) -> Self {
  651. self.envelope.user_data = user_data;
  652. self
  653. }
  654. /// Set the account version pins.
  655. pub fn account_snapshots(mut self, snapshots: Vec<AccountSnapshotId>) -> Self {
  656. self.envelope.account_snapshots = snapshots;
  657. self
  658. }
  659. /// Set the free-form metadata.
  660. pub fn metadata(mut self, metadata: Metadata) -> Self {
  661. self.envelope.metadata = metadata;
  662. self
  663. }
  664. /// Consume the builder and return the [`Envelope`].
  665. pub fn build(self) -> Envelope {
  666. self.envelope
  667. }
  668. }
  669. // ---------------------------------------------------------------------------
  670. // Account
  671. // ---------------------------------------------------------------------------
  672. /// Controls how much an account can spend beyond its posting-backed balance.
  673. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  674. pub enum AccountPolicy {
  675. /// Balance must stay >= 0.
  676. NoOverdraft,
  677. /// Balance must stay >= `floor` (floor < 0).
  678. CappedOverdraft {
  679. /// Minimum allowed balance (must be negative).
  680. floor: Cent,
  681. },
  682. /// No floor — the account can go arbitrarily negative.
  683. UncappedOverdraft,
  684. /// Fees, settlement, market-making, minting. No balance constraints.
  685. SystemAccount,
  686. /// Boundary account representing value entering/leaving the ledger; holds
  687. /// the offset (negative) side of deposits.
  688. ExternalAccount,
  689. }
  690. bitflags::bitflags! {
  691. /// Lifecycle and user-defined flags for an [`Account`].
  692. ///
  693. /// Bits 0–7 are reserved for system flags. Bits 8–31 are available for
  694. /// user-defined flags, which can be used with [`BookPolicy::allowed_flags`]
  695. /// to scope which accounts may participate in a book.
  696. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
  697. pub struct AccountFlags: u32 {
  698. /// Account may not be the source or destination of any transfer.
  699. const FROZEN = 1 << 0;
  700. /// Terminal — no further activity.
  701. const CLOSED = 1 << 1;
  702. // Bits 2–7: reserved for future system flags.
  703. // Bits 8–31: user-defined.
  704. /// User-defined flag 0.
  705. const USER_0 = 1 << 8;
  706. /// User-defined flag 1.
  707. const USER_1 = 1 << 9;
  708. /// User-defined flag 2.
  709. const USER_2 = 1 << 10;
  710. /// User-defined flag 3.
  711. const USER_3 = 1 << 11;
  712. /// User-defined flag 4.
  713. const USER_4 = 1 << 12;
  714. /// User-defined flag 5.
  715. const USER_5 = 1 << 13;
  716. /// User-defined flag 6.
  717. const USER_6 = 1 << 14;
  718. /// User-defined flag 7.
  719. const USER_7 = 1 << 15;
  720. }
  721. }
  722. /// A registered entity that must exist before it can transact.
  723. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  724. pub struct Account {
  725. /// Stable identity for this account.
  726. pub id: AccountId,
  727. /// Monotonically increasing version, starts at 1 on creation.
  728. pub version: u64,
  729. /// Overdraft / balance policy.
  730. pub policy: AccountPolicy,
  731. /// Lifecycle flags (frozen, closed).
  732. pub flags: AccountFlags,
  733. /// Book this entity belongs to.
  734. pub book: BookId,
  735. /// Fixed-width secondary identifiers.
  736. pub user_data: UserData,
  737. /// Free-form key-value metadata.
  738. pub metadata: Metadata,
  739. }
  740. impl Account {
  741. /// Create a version-1 account with the given policy: no flags, the default
  742. /// book, and empty user data / metadata. Convenience for the common case —
  743. /// set the other fields explicitly when you need them.
  744. pub fn new(id: AccountId, policy: AccountPolicy) -> Self {
  745. Self {
  746. id,
  747. version: 1,
  748. policy,
  749. flags: AccountFlags::empty(),
  750. book: DEFAULT_BOOK,
  751. user_data: UserData::default(),
  752. metadata: Metadata::new(),
  753. }
  754. }
  755. /// Returns `true` if the account has the `FROZEN` flag set.
  756. pub fn is_frozen(&self) -> bool {
  757. self.flags.contains(AccountFlags::FROZEN)
  758. }
  759. /// Returns `true` if the account has the `CLOSED` flag set.
  760. pub fn is_closed(&self) -> bool {
  761. self.flags.contains(AccountFlags::CLOSED)
  762. }
  763. }
  764. // ---------------------------------------------------------------------------
  765. // Receipt
  766. // ---------------------------------------------------------------------------
  767. /// Confirmation of a committed transfer, carrying its content-addressed id.
  768. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  769. pub struct Receipt {
  770. /// Content-addressed id of the committed transfer.
  771. pub transfer_id: EnvelopeId,
  772. }
  773. // ---------------------------------------------------------------------------
  774. // Transfer — intent-based API
  775. // ---------------------------------------------------------------------------
  776. /// A single movement within a transfer: move value from one account to another.
  777. ///
  778. /// Every operation (pay, deposit, withdraw) is expressed as one or more
  779. /// movements. The resolve step aggregates net debits per account and selects
  780. /// postings only for accounts with a positive net debit.
  781. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  782. pub struct Movement {
  783. /// Account being debited.
  784. pub from: AccountId,
  785. /// Account being credited.
  786. pub to: AccountId,
  787. /// Asset to transfer.
  788. pub asset: AssetId,
  789. /// Amount to transfer (may be negative for offset postings).
  790. pub amount: Cent,
  791. }
  792. /// A transfer intent — one or more movements to execute atomically.
  793. ///
  794. /// The saga pipeline resolves movements into concrete postings ([`Envelope`])
  795. /// during execution. Callers express *what* should happen, not *which postings*
  796. /// to consume.
  797. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
  798. pub struct Transfer {
  799. /// Movements to execute atomically.
  800. pub movements: Vec<Movement>,
  801. /// Book this entity belongs to.
  802. pub book: BookId,
  803. /// Fixed-width secondary identifiers.
  804. pub user_data: UserData,
  805. /// Free-form key-value metadata.
  806. pub metadata: Metadata,
  807. }
  808. /// Builder for constructing [`Transfer`] values.
  809. #[derive(Default)]
  810. pub struct TransferBuilder {
  811. transfer: Transfer,
  812. }
  813. impl TransferBuilder {
  814. /// Create an empty builder.
  815. pub fn new() -> Self {
  816. Self::default()
  817. }
  818. /// Add a raw movement.
  819. pub fn movement(
  820. mut self,
  821. from: AccountId,
  822. to: AccountId,
  823. asset: AssetId,
  824. amount: Cent,
  825. ) -> Self {
  826. self.transfer.movements.push(Movement {
  827. from,
  828. to,
  829. asset,
  830. amount,
  831. });
  832. self
  833. }
  834. /// Add a pay movement: transfer value between two accounts.
  835. pub fn pay(self, from: AccountId, to: AccountId, asset: AssetId, amount: Cent) -> Self {
  836. self.movement(from, to, asset, amount)
  837. }
  838. /// Add a deposit: creates an offset posting on the external account and
  839. /// credits the target account. Pushes two movements whose net debit on the
  840. /// external account is zero.
  841. pub fn deposit(
  842. self,
  843. to: AccountId,
  844. asset: AssetId,
  845. amount: Cent,
  846. external: AccountId,
  847. ) -> Result<Self, OverflowError> {
  848. let neg = amount.checked_neg()?;
  849. Ok(self
  850. .movement(external, external, asset, neg)
  851. .movement(external, to, asset, amount))
  852. }
  853. /// Add a withdrawal: move value from an account to an external destination.
  854. pub fn withdraw(
  855. self,
  856. from: AccountId,
  857. asset: AssetId,
  858. amount: Cent,
  859. external: AccountId,
  860. ) -> Self {
  861. self.movement(from, external, asset, amount)
  862. }
  863. /// Set the book.
  864. pub fn book(mut self, book: BookId) -> Self {
  865. self.transfer.book = book;
  866. self
  867. }
  868. /// Set the fixed-width secondary identifiers.
  869. pub fn user_data(mut self, user_data: UserData) -> Self {
  870. self.transfer.user_data = user_data;
  871. self
  872. }
  873. /// Set the free-form metadata.
  874. pub fn metadata(mut self, metadata: Metadata) -> Self {
  875. self.transfer.metadata = metadata;
  876. self
  877. }
  878. /// Consume the builder and return the [`Transfer`].
  879. pub fn build(self) -> Transfer {
  880. self.transfer
  881. }
  882. }
  883. // ---------------------------------------------------------------------------
  884. // ToBytes implementations
  885. // ---------------------------------------------------------------------------
  886. impl ToBytes for AccountId {
  887. fn to_bytes(&self) -> Vec<u8> {
  888. self.0.to_be_bytes().to_vec()
  889. }
  890. }
  891. impl ToBytes for AccountSnapshotId {
  892. fn to_bytes(&self) -> Vec<u8> {
  893. let mut buf = Vec::with_capacity(40);
  894. buf.extend_from_slice(&self.account.0.to_be_bytes());
  895. buf.extend_from_slice(&self.snapshot_id);
  896. buf
  897. }
  898. }
  899. impl ToBytes for AssetId {
  900. fn to_bytes(&self) -> Vec<u8> {
  901. self.0.to_be_bytes().to_vec()
  902. }
  903. }
  904. impl ToBytes for EnvelopeId {
  905. fn to_bytes(&self) -> Vec<u8> {
  906. self.0.to_vec()
  907. }
  908. }
  909. impl ToBytes for PostingId {
  910. fn to_bytes(&self) -> Vec<u8> {
  911. let mut buf = Vec::with_capacity(34);
  912. buf.extend_from_slice(&self.transfer.0);
  913. write_u16(&mut buf, self.index);
  914. buf
  915. }
  916. }
  917. impl ToBytes for UserData {
  918. fn to_bytes(&self) -> Vec<u8> {
  919. let mut buf = Vec::with_capacity(28);
  920. write_u128(&mut buf, self.d128);
  921. write_u64(&mut buf, self.d64);
  922. write_u32(&mut buf, self.d32);
  923. buf
  924. }
  925. }
  926. impl ToBytes for AccountPolicy {
  927. fn to_bytes(&self) -> Vec<u8> {
  928. let mut buf = Vec::with_capacity(9);
  929. match self {
  930. Self::NoOverdraft => buf.push(0),
  931. Self::CappedOverdraft { floor } => {
  932. buf.push(1);
  933. buf.extend(floor.to_bytes());
  934. }
  935. Self::UncappedOverdraft => buf.push(2),
  936. Self::SystemAccount => buf.push(3),
  937. Self::ExternalAccount => buf.push(4),
  938. }
  939. buf
  940. }
  941. }
  942. impl ToBytes for AccountFlags {
  943. fn to_bytes(&self) -> Vec<u8> {
  944. self.bits().to_be_bytes().to_vec()
  945. }
  946. }
  947. impl ToBytes for BookId {
  948. fn to_bytes(&self) -> Vec<u8> {
  949. self.0.to_be_bytes().to_vec()
  950. }
  951. }
  952. impl ToBytes for NewPosting {
  953. fn to_bytes(&self) -> Vec<u8> {
  954. let mut buf = Vec::new();
  955. buf.extend(self.owner.to_bytes());
  956. buf.extend_from_slice(&self.asset.0.to_be_bytes());
  957. buf.extend(self.value.to_bytes());
  958. match &self.payer {
  959. Some(p) => {
  960. buf.push(1);
  961. buf.extend(p.to_bytes());
  962. }
  963. None => buf.push(0),
  964. }
  965. buf
  966. }
  967. }
  968. impl ToBytes for Posting {
  969. fn to_bytes(&self) -> Vec<u8> {
  970. let mut buf = Vec::new();
  971. buf.extend(self.id.to_bytes());
  972. buf.extend(self.owner.to_bytes());
  973. buf.extend_from_slice(&self.asset.0.to_be_bytes());
  974. buf.extend(self.value.to_bytes());
  975. buf.push(match self.status {
  976. PostingStatus::Active => 0,
  977. PostingStatus::PendingInactive => 1,
  978. PostingStatus::Inactive => 2,
  979. });
  980. buf
  981. }
  982. }
  983. impl ToBytes for Envelope {
  984. fn to_bytes(&self) -> Vec<u8> {
  985. let mut buf = Vec::new();
  986. buf.push(CANONICAL_VERSION);
  987. write_u32(&mut buf, self.consumes.len() as u32);
  988. for pid in &self.consumes {
  989. buf.extend(pid.to_bytes());
  990. }
  991. write_u32(&mut buf, self.creates.len() as u32);
  992. for np in &self.creates {
  993. buf.extend(np.to_bytes());
  994. }
  995. write_u32(&mut buf, self.account_snapshots.len() as u32);
  996. for snap in &self.account_snapshots {
  997. buf.extend(snap.to_bytes());
  998. }
  999. buf.extend(self.book.to_bytes());
  1000. buf.extend(self.user_data.to_bytes());
  1001. write_u32(&mut buf, self.metadata.len() as u32);
  1002. for (key, value) in &self.metadata {
  1003. let key_bytes = key.as_bytes();
  1004. write_u32(&mut buf, key_bytes.len() as u32);
  1005. buf.extend_from_slice(key_bytes);
  1006. write_u32(&mut buf, value.len() as u32);
  1007. buf.extend_from_slice(value);
  1008. }
  1009. buf
  1010. }
  1011. }
  1012. impl ToBytes for Account {
  1013. fn to_bytes(&self) -> Vec<u8> {
  1014. let mut buf = Vec::new();
  1015. buf.push(CANONICAL_VERSION);
  1016. buf.extend(self.id.to_bytes());
  1017. write_u64(&mut buf, self.version);
  1018. buf.extend(self.policy.to_bytes());
  1019. buf.extend(self.flags.to_bytes());
  1020. buf.extend(self.book.to_bytes());
  1021. buf.extend(self.user_data.to_bytes());
  1022. write_u32(&mut buf, self.metadata.len() as u32);
  1023. for (key, value) in &self.metadata {
  1024. let key_bytes = key.as_bytes();
  1025. write_u32(&mut buf, key_bytes.len() as u32);
  1026. buf.extend_from_slice(key_bytes);
  1027. write_u32(&mut buf, value.len() as u32);
  1028. buf.extend_from_slice(value);
  1029. }
  1030. buf
  1031. }
  1032. }
  1033. impl ToBytes for Receipt {
  1034. fn to_bytes(&self) -> Vec<u8> {
  1035. self.transfer_id.0.to_vec()
  1036. }
  1037. }