integration.rs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716
  1. #![allow(missing_docs)]
  2. use std::sync::Arc;
  3. use kuatia::ledger::Ledger;
  4. use kuatia::mem_store::InMemoryStore;
  5. use kuatia_core::*;
  6. use std::collections::BTreeMap;
  7. fn usd() -> AssetId {
  8. AssetId::new(1)
  9. }
  10. fn eur() -> AssetId {
  11. AssetId::new(2)
  12. }
  13. fn account(id: i64) -> AccountId {
  14. AccountId::new(id)
  15. }
  16. fn external() -> AccountId {
  17. AccountId::new(99)
  18. }
  19. fn make_account(id: i64, policy: AccountPolicy) -> Account {
  20. Account {
  21. id: AccountId::new(id),
  22. version: 1,
  23. policy,
  24. flags: AccountFlags::empty(),
  25. book: 0,
  26. code: 0,
  27. user_data: UserData::default(),
  28. metadata: BTreeMap::new(),
  29. }
  30. }
  31. async fn setup_ledger() -> Arc<Ledger> {
  32. let store = InMemoryStore::new();
  33. let ledger = Arc::new(Ledger::new(store));
  34. ledger
  35. .store()
  36. .create_account(make_account(1, AccountPolicy::NoOverdraft))
  37. .await
  38. .unwrap();
  39. ledger
  40. .store()
  41. .create_account(make_account(2, AccountPolicy::NoOverdraft))
  42. .await
  43. .unwrap();
  44. ledger
  45. .store()
  46. .create_account(make_account(3, AccountPolicy::NoOverdraft))
  47. .await
  48. .unwrap();
  49. ledger
  50. .store()
  51. .create_account(make_account(99, AccountPolicy::ExternalAccount))
  52. .await
  53. .unwrap();
  54. ledger
  55. }
  56. /// Helper: deposit via commit()
  57. async fn deposit(
  58. ledger: &Arc<Ledger>,
  59. to: AccountId,
  60. asset: AssetId,
  61. amount: Cent,
  62. ext: AccountId,
  63. ) -> Receipt {
  64. let transfer = TransferBuilder::new()
  65. .deposit(to, asset, amount, ext)
  66. .unwrap()
  67. .build();
  68. ledger.commit(transfer).await.unwrap()
  69. }
  70. /// Helper: pay via commit()
  71. async fn pay(
  72. ledger: &Arc<Ledger>,
  73. from: AccountId,
  74. to: AccountId,
  75. asset: AssetId,
  76. amount: Cent,
  77. ) -> Receipt {
  78. let transfer = TransferBuilder::new().pay(from, to, asset, amount).build();
  79. ledger.commit(transfer).await.unwrap()
  80. }
  81. /// Helper: withdraw via commit()
  82. async fn withdraw(
  83. ledger: &Arc<Ledger>,
  84. from: AccountId,
  85. asset: AssetId,
  86. amount: Cent,
  87. ext: AccountId,
  88. ) -> Receipt {
  89. let transfer = TransferBuilder::new()
  90. .withdraw(from, asset, amount, ext)
  91. .build();
  92. ledger.commit(transfer).await.unwrap()
  93. }
  94. // ---------------------------------------------------------------------------
  95. // §4.1 Deposit
  96. // ---------------------------------------------------------------------------
  97. #[tokio::test]
  98. async fn deposit_creates_balanced_postings() {
  99. let ledger = setup_ledger().await;
  100. deposit(&ledger, account(1), usd(), Cent::from(100), external()).await;
  101. assert_eq!(
  102. ledger.balance(&account(1), &usd()).await.unwrap(),
  103. Cent::from(100)
  104. );
  105. assert_eq!(
  106. ledger.balance(&external(), &usd()).await.unwrap(),
  107. Cent::from(-100)
  108. );
  109. }
  110. // ---------------------------------------------------------------------------
  111. // §4.2 Internal transfer with change
  112. // ---------------------------------------------------------------------------
  113. #[tokio::test]
  114. async fn pay_with_change() {
  115. let ledger = setup_ledger().await;
  116. deposit(&ledger, account(1), usd(), Cent::from(100), external()).await;
  117. pay(&ledger, account(1), account(2), usd(), Cent::from(50)).await;
  118. assert_eq!(
  119. ledger.balance(&account(1), &usd()).await.unwrap(),
  120. Cent::from(50)
  121. );
  122. assert_eq!(
  123. ledger.balance(&account(2), &usd()).await.unwrap(),
  124. Cent::from(50)
  125. );
  126. assert_eq!(
  127. ledger.balance(&external(), &usd()).await.unwrap(),
  128. Cent::from(-100)
  129. );
  130. }
  131. // ---------------------------------------------------------------------------
  132. // §4.3 Multi-hop
  133. // ---------------------------------------------------------------------------
  134. #[tokio::test]
  135. async fn multi_hop_transfer() {
  136. let ledger = setup_ledger().await;
  137. deposit(&ledger, account(1), usd(), Cent::from(100), external()).await;
  138. pay(&ledger, account(1), account(2), usd(), Cent::from(50)).await;
  139. pay(&ledger, account(2), account(3), usd(), Cent::from(20)).await;
  140. assert_eq!(
  141. ledger.balance(&account(1), &usd()).await.unwrap(),
  142. Cent::from(50)
  143. );
  144. assert_eq!(
  145. ledger.balance(&account(2), &usd()).await.unwrap(),
  146. Cent::from(30)
  147. );
  148. assert_eq!(
  149. ledger.balance(&account(3), &usd()).await.unwrap(),
  150. Cent::from(20)
  151. );
  152. assert_eq!(
  153. ledger.balance(&external(), &usd()).await.unwrap(),
  154. Cent::from(-100)
  155. );
  156. }
  157. // ---------------------------------------------------------------------------
  158. // §4.5 Withdrawal
  159. // ---------------------------------------------------------------------------
  160. #[tokio::test]
  161. async fn withdrawal_reduces_external_liability() {
  162. let ledger = setup_ledger().await;
  163. deposit(&ledger, account(1), usd(), Cent::from(100), external()).await;
  164. withdraw(&ledger, account(1), usd(), Cent::from(50), external()).await;
  165. assert_eq!(
  166. ledger.balance(&account(1), &usd()).await.unwrap(),
  167. Cent::from(50)
  168. );
  169. assert_eq!(
  170. ledger.balance(&external(), &usd()).await.unwrap(),
  171. Cent::from(-50)
  172. );
  173. }
  174. // ---------------------------------------------------------------------------
  175. // Full round-trip: deposit -> pay -> withdraw -> verify total = 0
  176. // ---------------------------------------------------------------------------
  177. #[tokio::test]
  178. async fn full_round_trip() {
  179. let ledger = setup_ledger().await;
  180. deposit(&ledger, account(1), usd(), Cent::from(100), external()).await;
  181. pay(&ledger, account(1), account(2), usd(), Cent::from(60)).await;
  182. withdraw(&ledger, account(2), usd(), Cent::from(60), external()).await;
  183. withdraw(&ledger, account(1), usd(), Cent::from(40), external()).await;
  184. assert_eq!(
  185. ledger.balance(&account(1), &usd()).await.unwrap(),
  186. Cent::ZERO
  187. );
  188. assert_eq!(
  189. ledger.balance(&account(2), &usd()).await.unwrap(),
  190. Cent::ZERO
  191. );
  192. assert_eq!(
  193. ledger.balance(&external(), &usd()).await.unwrap(),
  194. Cent::ZERO
  195. );
  196. }
  197. // ---------------------------------------------------------------------------
  198. // Idempotency -- committing same envelope twice returns same receipt
  199. // ---------------------------------------------------------------------------
  200. #[tokio::test]
  201. async fn idempotent_commit() {
  202. let ledger = setup_ledger().await;
  203. let envelope = EnvelopeBuilder::new()
  204. .creates(vec![
  205. NewPosting {
  206. owner: account(1),
  207. asset: usd(),
  208. value: Cent::from(100),
  209. payer: None,
  210. },
  211. NewPosting {
  212. owner: external(),
  213. asset: usd(),
  214. value: Cent::from(-100),
  215. payer: None,
  216. },
  217. ])
  218. .build();
  219. let r1 = ledger.commit_atomic(envelope.clone()).await.unwrap();
  220. let r2 = ledger.commit_atomic(envelope).await.unwrap();
  221. assert_eq!(r1.transfer_id, r2.transfer_id);
  222. // Balance should only be 100, not 200 (second commit was a no-op)
  223. assert_eq!(
  224. ledger.balance(&account(1), &usd()).await.unwrap(),
  225. Cent::from(100)
  226. );
  227. }
  228. // ---------------------------------------------------------------------------
  229. // Overdraft prevention
  230. // ---------------------------------------------------------------------------
  231. #[tokio::test]
  232. async fn overdraft_rejected() {
  233. let ledger = setup_ledger().await;
  234. deposit(&ledger, account(1), usd(), Cent::from(50), external()).await;
  235. let transfer = TransferBuilder::new()
  236. .pay(account(1), account(2), usd(), Cent::from(100))
  237. .build();
  238. let result = ledger.commit(transfer).await;
  239. assert!(result.is_err());
  240. // Balance unchanged
  241. assert_eq!(
  242. ledger.balance(&account(1), &usd()).await.unwrap(),
  243. Cent::from(50)
  244. );
  245. }
  246. // ---------------------------------------------------------------------------
  247. // Reverse: forward compensating transfer
  248. // ---------------------------------------------------------------------------
  249. #[tokio::test]
  250. async fn reverse_restores_balances() {
  251. let ledger = setup_ledger().await;
  252. deposit(&ledger, account(1), usd(), Cent::from(100), external()).await;
  253. let pay_receipt = pay(&ledger, account(1), account(2), usd(), Cent::from(60)).await;
  254. assert_eq!(
  255. ledger.balance(&account(1), &usd()).await.unwrap(),
  256. Cent::from(40)
  257. );
  258. assert_eq!(
  259. ledger.balance(&account(2), &usd()).await.unwrap(),
  260. Cent::from(60)
  261. );
  262. // Reverse the payment
  263. ledger.reverse(&pay_receipt.transfer_id).await.unwrap();
  264. assert_eq!(
  265. ledger.balance(&account(1), &usd()).await.unwrap(),
  266. Cent::from(100)
  267. );
  268. assert_eq!(
  269. ledger.balance(&account(2), &usd()).await.unwrap(),
  270. Cent::ZERO
  271. );
  272. }
  273. // ---------------------------------------------------------------------------
  274. // Frozen account blocks transfers
  275. // ---------------------------------------------------------------------------
  276. #[tokio::test]
  277. async fn frozen_account_rejected() {
  278. let store = InMemoryStore::new();
  279. let ledger = Arc::new(Ledger::new(store));
  280. let mut frozen = make_account(1, AccountPolicy::NoOverdraft);
  281. frozen.flags = AccountFlags::FROZEN;
  282. ledger.store().create_account(frozen).await.unwrap();
  283. ledger
  284. .store()
  285. .create_account(make_account(99, AccountPolicy::ExternalAccount))
  286. .await
  287. .unwrap();
  288. let transfer = TransferBuilder::new()
  289. .deposit(account(1), usd(), Cent::from(100), external())
  290. .unwrap()
  291. .build();
  292. let result = ledger.commit(transfer).await;
  293. assert!(result.is_err());
  294. }
  295. // ---------------------------------------------------------------------------
  296. // Multi-asset: each asset conserves independently
  297. // ---------------------------------------------------------------------------
  298. #[tokio::test]
  299. async fn multi_asset_independent_balances() {
  300. let ledger = setup_ledger().await;
  301. deposit(&ledger, account(1), usd(), Cent::from(100), external()).await;
  302. deposit(&ledger, account(1), eur(), Cent::from(200), external()).await;
  303. assert_eq!(
  304. ledger.balance(&account(1), &usd()).await.unwrap(),
  305. Cent::from(100)
  306. );
  307. assert_eq!(
  308. ledger.balance(&account(1), &eur()).await.unwrap(),
  309. Cent::from(200)
  310. );
  311. pay(&ledger, account(1), account(2), usd(), Cent::from(30)).await;
  312. assert_eq!(
  313. ledger.balance(&account(1), &usd()).await.unwrap(),
  314. Cent::from(70)
  315. );
  316. assert_eq!(
  317. ledger.balance(&account(1), &eur()).await.unwrap(),
  318. Cent::from(200)
  319. );
  320. assert_eq!(
  321. ledger.balance(&account(2), &usd()).await.unwrap(),
  322. Cent::from(30)
  323. );
  324. }
  325. // ---------------------------------------------------------------------------
  326. // §4.4 FX trade via market account
  327. // ---------------------------------------------------------------------------
  328. #[tokio::test]
  329. async fn fx_trade_via_market_account() {
  330. let store = InMemoryStore::new();
  331. let ledger = Arc::new(Ledger::new(store));
  332. // Setup accounts
  333. for (id, policy) in [
  334. (1, AccountPolicy::NoOverdraft),
  335. (50, AccountPolicy::SystemAccount), // FX market account
  336. (99, AccountPolicy::ExternalAccount),
  337. ] {
  338. ledger
  339. .store()
  340. .create_account(make_account(id, policy))
  341. .await
  342. .unwrap();
  343. }
  344. // Seed: account1 has 100 USD, fx has 92 EUR
  345. deposit(&ledger, account(1), usd(), Cent::from(100), external()).await;
  346. deposit(&ledger, account(50), eur(), Cent::from(92), external()).await;
  347. // FX trade: account1 sells 100 USD, buys 92 EUR
  348. // Build the atomic envelope manually since it spans two assets
  349. let a1_usd_postings = ledger
  350. .store()
  351. .get_postings_by_account(&account(1), Some(&usd()), Some(PostingStatus::Active))
  352. .await
  353. .unwrap();
  354. let fx_eur_postings = ledger
  355. .store()
  356. .get_postings_by_account(&account(50), Some(&eur()), Some(PostingStatus::Active))
  357. .await
  358. .unwrap();
  359. let envelope = EnvelopeBuilder::new()
  360. .consumes(vec![a1_usd_postings[0].id, fx_eur_postings[0].id])
  361. .creates(vec![
  362. NewPosting {
  363. owner: account(50),
  364. asset: usd(),
  365. value: Cent::from(100),
  366. payer: Some(account(1)),
  367. },
  368. NewPosting {
  369. owner: account(1),
  370. asset: eur(),
  371. value: Cent::from(92),
  372. payer: Some(account(50)),
  373. },
  374. ])
  375. .build();
  376. ledger.commit_atomic(envelope).await.unwrap();
  377. // Verify
  378. assert_eq!(
  379. ledger.balance(&account(1), &usd()).await.unwrap(),
  380. Cent::ZERO
  381. );
  382. assert_eq!(
  383. ledger.balance(&account(1), &eur()).await.unwrap(),
  384. Cent::from(92)
  385. );
  386. assert_eq!(
  387. ledger.balance(&account(50), &usd()).await.unwrap(),
  388. Cent::from(100)
  389. );
  390. assert_eq!(
  391. ledger.balance(&account(50), &eur()).await.unwrap(),
  392. Cent::ZERO
  393. );
  394. }
  395. // ---------------------------------------------------------------------------
  396. // Account lifecycle: freeze / unfreeze / close
  397. // ---------------------------------------------------------------------------
  398. #[tokio::test]
  399. async fn freeze_blocks_transfers() {
  400. let ledger = setup_ledger().await;
  401. deposit(&ledger, account(1), usd(), Cent::from(100), external()).await;
  402. ledger.freeze(&account(1)).await.unwrap();
  403. // Paying from a frozen account should fail
  404. let transfer = TransferBuilder::new()
  405. .pay(account(1), account(2), usd(), Cent::from(50))
  406. .build();
  407. let result = ledger.commit(transfer).await;
  408. assert!(result.is_err());
  409. // Balance unchanged
  410. assert_eq!(
  411. ledger.balance(&account(1), &usd()).await.unwrap(),
  412. Cent::from(100)
  413. );
  414. }
  415. #[tokio::test]
  416. async fn unfreeze_re_enables_transfers() {
  417. let ledger = setup_ledger().await;
  418. deposit(&ledger, account(1), usd(), Cent::from(100), external()).await;
  419. ledger.freeze(&account(1)).await.unwrap();
  420. ledger.unfreeze(&account(1)).await.unwrap();
  421. // Should work again
  422. pay(&ledger, account(1), account(2), usd(), Cent::from(50)).await;
  423. assert_eq!(
  424. ledger.balance(&account(1), &usd()).await.unwrap(),
  425. Cent::from(50)
  426. );
  427. }
  428. #[tokio::test]
  429. async fn close_account_with_zero_balance() {
  430. let ledger = setup_ledger().await;
  431. // Account 3 has never transacted -- zero balance, no postings
  432. ledger.close(&account(3)).await.unwrap();
  433. // Closed account rejects deposits
  434. let transfer = TransferBuilder::new()
  435. .deposit(account(3), usd(), Cent::from(100), external())
  436. .unwrap()
  437. .build();
  438. let result = ledger.commit(transfer).await;
  439. assert!(result.is_err());
  440. }
  441. #[tokio::test]
  442. async fn close_account_with_balance_rejected() {
  443. let ledger = setup_ledger().await;
  444. deposit(&ledger, account(1), usd(), Cent::from(100), external()).await;
  445. // Should fail -- account still has active postings
  446. let result = ledger.close(&account(1)).await;
  447. assert!(result.is_err());
  448. // Balance unchanged
  449. assert_eq!(
  450. ledger.balance(&account(1), &usd()).await.unwrap(),
  451. Cent::from(100)
  452. );
  453. }
  454. #[tokio::test]
  455. async fn freeze_closed_account_rejected() {
  456. let ledger = setup_ledger().await;
  457. ledger.close(&account(3)).await.unwrap();
  458. let result = ledger.freeze(&account(3)).await;
  459. assert!(result.is_err());
  460. }
  461. // ---------------------------------------------------------------------------
  462. // Query layer: history, postings, list_accounts, get_account
  463. // ---------------------------------------------------------------------------
  464. #[tokio::test]
  465. async fn history_returns_transfers_for_account() {
  466. let ledger = setup_ledger().await;
  467. deposit(&ledger, account(1), usd(), Cent::from(100), external()).await;
  468. pay(&ledger, account(1), account(2), usd(), Cent::from(40)).await;
  469. deposit(&ledger, account(2), usd(), Cent::from(50), external()).await;
  470. let h1 = ledger.history(&account(1)).await.unwrap();
  471. // account(1) was in the deposit and the pay
  472. assert_eq!(h1.len(), 2);
  473. let h2 = ledger.history(&account(2)).await.unwrap();
  474. // account(2) was in the pay and a second deposit
  475. assert_eq!(h2.len(), 2);
  476. }
  477. #[tokio::test]
  478. async fn postings_returns_all_postings() {
  479. let ledger = setup_ledger().await;
  480. deposit(&ledger, account(1), usd(), Cent::from(100), external()).await;
  481. pay(&ledger, account(1), account(2), usd(), Cent::from(60)).await;
  482. let posts = ledger.postings(&account(1)).await.unwrap();
  483. // Original 100 posting (now consumed) + 40 change posting (active)
  484. assert_eq!(posts.len(), 2);
  485. let active: Vec<_> = posts.iter().filter(|p| p.is_active()).collect();
  486. assert_eq!(active.len(), 1);
  487. assert_eq!(active[0].value, Cent::from(40));
  488. }
  489. #[tokio::test]
  490. async fn list_accounts_returns_all() {
  491. let ledger = setup_ledger().await;
  492. let accounts = ledger.list_accounts().await.unwrap();
  493. // setup_ledger creates accounts 1, 2, 3, 99
  494. assert_eq!(accounts.len(), 4);
  495. }
  496. #[tokio::test]
  497. async fn get_account_by_id() {
  498. let ledger = setup_ledger().await;
  499. let acc = ledger.get_account(&account(1)).await.unwrap();
  500. assert_eq!(acc.id, account(1));
  501. assert_eq!(acc.policy, AccountPolicy::NoOverdraft);
  502. }
  503. #[tokio::test]
  504. async fn get_account_not_found() {
  505. let ledger = setup_ledger().await;
  506. let result = ledger.get_account(&account(999)).await;
  507. assert!(result.is_err());
  508. }
  509. // ---------------------------------------------------------------------------
  510. // Append-only accounts: version history, version conflict, account_versions
  511. // ---------------------------------------------------------------------------
  512. #[tokio::test]
  513. async fn account_history_tracks_versions() {
  514. let ledger = setup_ledger().await;
  515. // Version 1: created
  516. let history = ledger.account_history(&account(1)).await.unwrap();
  517. assert_eq!(history.len(), 1);
  518. assert_eq!(history[0].version, 1);
  519. // Version 2: frozen
  520. ledger.freeze(&account(1)).await.unwrap();
  521. let history = ledger.account_history(&account(1)).await.unwrap();
  522. assert_eq!(history.len(), 2);
  523. assert_eq!(history[1].version, 2);
  524. assert!(history[1].is_frozen());
  525. // Version 3: unfrozen
  526. ledger.unfreeze(&account(1)).await.unwrap();
  527. let history = ledger.account_history(&account(1)).await.unwrap();
  528. assert_eq!(history.len(), 3);
  529. assert_eq!(history[2].version, 3);
  530. assert!(!history[2].is_frozen());
  531. }
  532. #[tokio::test]
  533. async fn store_never_compacts() {
  534. let ledger = setup_ledger().await;
  535. // Freeze and unfreeze multiple times
  536. for _ in 0..5 {
  537. ledger.freeze(&account(1)).await.unwrap();
  538. ledger.unfreeze(&account(1)).await.unwrap();
  539. }
  540. // All 11 versions preserved (1 creation + 10 mutations)
  541. let history = ledger.account_history(&account(1)).await.unwrap();
  542. assert_eq!(history.len(), 11);
  543. // Versions are monotonically increasing
  544. for (i, acc) in history.iter().enumerate() {
  545. assert_eq!(acc.version, (i + 1) as u64);
  546. }
  547. }
  548. #[tokio::test]
  549. async fn transfer_records_account_snapshots() {
  550. let ledger = setup_ledger().await;
  551. deposit(&ledger, account(1), usd(), Cent::from(100), external()).await;
  552. // The envelope should have account_snapshots populated by the resolve step
  553. let transfers = ledger.history(&account(1)).await.unwrap();
  554. assert_eq!(transfers.len(), 1);
  555. assert!(!transfers[0].envelope.account_snapshots().is_empty());
  556. }
  557. #[tokio::test]
  558. async fn stale_snapshot_rejected() {
  559. let ledger = setup_ledger().await;
  560. // Get current snapshot for account(1)
  561. let acc1 = ledger.get_account(&account(1)).await.unwrap();
  562. let stale_snapshot = kuatia_core::account_snapshot_id(&acc1);
  563. // Freeze account(1) -- changes its snapshot hash
  564. ledger.freeze(&account(1)).await.unwrap();
  565. // Build an envelope with the stale snapshot
  566. let envelope = EnvelopeBuilder::new()
  567. .creates(vec![
  568. NewPosting {
  569. owner: account(1),
  570. asset: usd(),
  571. value: Cent::from(100),
  572. payer: None,
  573. },
  574. NewPosting {
  575. owner: external(),
  576. asset: usd(),
  577. value: Cent::from(-100),
  578. payer: None,
  579. },
  580. ])
  581. .account_snapshots(vec![stale_snapshot])
  582. .build();
  583. let result = ledger.commit_atomic(envelope).await;
  584. assert!(result.is_err());
  585. }
  586. #[tokio::test]
  587. async fn account_hash_deterministic() {
  588. let acc = make_account(42, AccountPolicy::NoOverdraft);
  589. let h1 = kuatia_core::account_hash(&acc);
  590. let h2 = kuatia_core::account_hash(&acc);
  591. assert_eq!(h1, h2);
  592. }
  593. #[tokio::test]
  594. async fn account_hash_changes_with_version() {
  595. let mut acc = make_account(42, AccountPolicy::NoOverdraft);
  596. let h1 = kuatia_core::account_hash(&acc);
  597. acc.version = 2;
  598. acc.flags |= AccountFlags::FROZEN;
  599. let h2 = kuatia_core::account_hash(&acc);
  600. assert_ne!(h1, h2);
  601. }