#![allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
use doubleentry::account::{AccountId, AccountRegistry};
use doubleentry::balance::TrialBalance;
use doubleentry::canonical::Canonical;
use doubleentry::entry::{Draft, LedgerPolicy, SealContext};
use doubleentry::merkle::TreeHead;
use doubleentry::merkle::{MerkleLog, empty_root, leaf_hash};
use doubleentry::period::{LedgerId, PeriodCalendar, PeriodId};
use doubleentry::seal::{PeriodCoverage, Seal};
use doubleentry::{
Amount, Balanced, Currency, Description, Dimensions, DocumentRef, Entry, EntryId, Hash,
IdempotencyKey, Label, Posting, Provenance,
};
use time::macros::date;
use uuid::Uuid;
type Eur = Amount<2>;
fn label(s: &str) -> Label {
Label::new(s).expect("valid label")
}
fn leaf(i: u64) -> Hash {
let mut bytes = [0u8; 32];
bytes[..8].copy_from_slice(&i.to_le_bytes());
Hash::from_bytes(bytes)
}
fn reference_entry() -> Entry<Balanced, 2> {
let mut accounts = AccountRegistry::new();
let cash = accounts
.register_path("Assets:Cash", date!(2020 - 01 - 01))
.expect("registers");
let revenue = accounts
.register_path("Income:Sales", date!(2020 - 01 - 01))
.expect("registers");
let calendar = PeriodCalendar::new();
let policy = LedgerPolicy::default();
let ctx = SealContext {
accounts: &accounts,
calendar: &calendar,
policy: &policy,
};
let dimensions = Dimensions::none()
.with(label("activity"), label("Network"))
.expect("fits")
.with(label("segment"), label("Electricity"))
.expect("fits");
let id = EntryId::from_uuid(Uuid::from_bytes([0x11; 16]));
let original = EntryId::from_uuid(Uuid::from_bytes([0x22; 16]));
Entry::<Draft, 2>::new(
id,
IdempotencyKey::new(b"golden-vector-key".to_vec()).expect("valid"),
date!(2026 - 03 - 15),
)
.with_value_date(date!(2026 - 03 - 17))
.with_description(Description::new("reference entry").expect("valid"))
.with_provenance(
Provenance::none()
.with_actor("auditor")
.expect("valid")
.with_source("golden")
.expect("valid"),
)
.with_document(DocumentRef::new("INV-2026-0001", Hash::from_bytes([0x33; 32])).expect("valid"))
.reversing(original, date!(2026 - 02 - 01))
.post(Posting::debit(cash, Eur::from_minor(119_000), Currency::EUR).with_dimensions(dimensions))
.post(Posting::credit(
revenue,
Eur::from_minor(119_000),
Currency::EUR,
))
.seal(&ctx)
.expect("balances")
}
#[test]
fn canonical_encoding_of_the_reference_entry_is_unchanged() {
let encoded = reference_entry().to_canonical_bytes();
let hex: String = encoded.iter().map(|b| format!("{b:02x}")).collect();
let expected = concat!(
"021100000000000000676f6c64656e2d766563746f722d6b6579ea070000030fea0700",
"0003110f000000000000007265666572656e636520656e747279020000000000000000",
"00000000d8d00100000000004555520002000000000000000800000000000000616374",
"697669747907000000000000004e6574776f726b07000000000000007365676d656e74",
"0b00000000000000456c6563747269636974790100000001d8d0010000000000455552",
"0000000000000000000001070000000000000061756469746f72010600000000000000",
"676f6c64656e00010d00000000000000494e562d323032362d30303031013333333333",
"3333333333333333333333333333333333333333333333333333330122222222222222",
"22222222222222222201ea0700000201",
);
assert_eq!(
hex, expected,
"the canonical encoding of an entry changed; see this file's module docs"
);
}
#[test]
fn the_reference_entry_content_hash_is_unchanged() {
assert_eq!(
reference_entry().content_hash().to_hex(),
"5bd373dc4d90adb1aa7e8c7f665fbf0a6eac43b6e8980272f93f41e17c8eac2c",
"the entry content hash changed; see this file's module docs"
);
}
fn reference_seal() -> Seal {
let mut tb = TrialBalance::<2>::new();
let mut accounts = AccountRegistry::new();
let cash = accounts
.register_path("Assets:Cash", date!(2020 - 01 - 01))
.expect("registers");
let revenue = accounts
.register_path("Income:Sales", date!(2020 - 01 - 01))
.expect("registers");
tb.apply(&Posting::debit(
cash,
Eur::from_minor(119_000),
Currency::EUR,
))
.expect("fits");
tb.apply(&Posting::credit(
revenue,
Eur::from_minor(119_000),
Currency::EUR,
))
.expect("fits");
Seal::build::<2>(
LedgerId::new("golden-ledger").expect("valid"),
PeriodId::new("2026-03").expect("valid"),
PeriodCoverage::spanning(0, 3, 4),
TreeHead {
size: 4,
root: Hash::from_bytes([0x44; 32]),
},
&tb,
accounts.commitment(),
Some(Hash::from_bytes([0x55; 32])),
)
}
#[test]
fn the_reference_seal_hash_is_unchanged() {
assert_eq!(
reference_seal().seal_hash.to_hex(),
"ab58bc1acffe46e432bf8959af14aaa64281622a67d41a7d8ce53a534b8207d9",
"the seal hash changed; see this file's module docs"
);
}
fn reference_registry() -> AccountRegistry {
let mut accounts = AccountRegistry::new();
accounts
.register_path("Assets:Cash", date!(2020 - 01 - 01))
.expect("registers");
accounts
.register_path("Income:Sales", date!(2020 - 01 - 01))
.expect("registers");
accounts
}
#[test]
fn the_account_binding_commitment_is_unchanged() {
assert_eq!(
reference_registry().commitment().to_hex(),
"65ca7c50e7235ac82ea3eef1acd66e1d90abe8dc2527617bfe81e234a1416a5a",
"the account binding commitment changed; see this file\'s module docs"
);
}
#[test]
fn a_balance_limit_moves_the_binding_commitment() {
let mut relaxed = reference_registry();
relaxed
.set_limit(
AccountId::from_index(0),
doubleentry::account::BalanceLimit::NoCreditBalance,
)
.expect("registered");
assert_ne!(relaxed.commitment(), reference_registry().commitment());
}
#[test]
fn a_document_reference_without_a_hash_encodes_differently() {
let hashed = DocumentRef::new("INV-1", Hash::from_bytes([0x33; 32])).expect("valid");
let unverified = DocumentRef::unverified("INV-1").expect("valid");
assert!(hashed.is_verifiable());
assert!(!unverified.is_verifiable());
assert_ne!(
hashed.to_canonical_bytes(),
unverified.to_canonical_bytes(),
"an unhashed document reference must not encode as a hashed one"
);
}
#[test]
fn merkle_constants_are_unchanged() {
assert_eq!(
empty_root().to_hex(),
"854ee3641f62b1063d0eee1b9a4f6f872b38625e3f4a090dd5a9e5c58d04ebf9"
);
assert_eq!(
leaf_hash(&leaf(0)).to_hex(),
"db5c7a5ad0a6c6654868caba0dafda6a794f4278cb5c4573728dc645d31dd632"
);
}
#[test]
fn merkle_roots_for_known_sizes_are_unchanged() {
let expected = [
"db5c7a5ad0a6c6654868caba0dafda6a794f4278cb5c4573728dc645d31dd632",
"b8581ec29a5787add8ade15bdbb4f3e2412222c6600b1195f659dd82d43d25d4",
"4846c0dda0fe1aa609475f8287ac34a09d3f5e8d3a2812d275359f6977989d12",
"c8fdd4a2a60ada5bad1bd77a1a1407370e53db996cd109c3b578f21ee0040ffc",
"6bc34b2a6f4e608393c18d44b21493be394757cde46cb361caae9544fc95804b",
"c01bef11185172ec477ffab55f45d97ef5a789bcfbb6091792556fb5de13add0",
];
let sizes = [1u64, 2, 3, 4, 5, 8];
for (size, want) in sizes.iter().zip(expected.iter()) {
let log = MerkleLog::from_leaves((0..*size).map(leaf).collect());
assert_eq!(log.root().to_hex(), *want, "root changed for size {size}");
}
}
#[test]
fn money_formatting_is_unchanged() {
assert_eq!(Eur::from_minor(119_000).to_string(), "1190.00");
assert_eq!(Eur::from_minor(-5).to_string(), "-0.05");
assert_eq!(Eur::from_minor(0).to_string(), "0.00");
assert_eq!(Amount::<0>::from_minor(7).to_string(), "7");
assert_eq!(Amount::<5>::from_minor(123).to_string(), "0.00123");
}
#[test]
#[ignore = "prints current values for regenerating the vectors above"]
fn emit_vectors() {
let e = reference_entry();
let hex: String = e
.to_canonical_bytes()
.iter()
.map(|b| format!("{b:02x}"))
.collect();
println!("ENCODING={hex}");
println!("ENTRY_HASH={}", e.content_hash().to_hex());
println!("SEAL_HASH={}", reference_seal().seal_hash.to_hex());
println!(
"ACCOUNTS_ROOT={}",
reference_registry().commitment().to_hex()
);
println!("EMPTY_ROOT={}", empty_root().to_hex());
println!("LEAF_ZERO={}", leaf_hash(&leaf(0)).to_hex());
for size in [1u64, 2, 3, 4, 5, 8] {
let log = MerkleLog::from_leaves((0..size).map(leaf).collect());
println!("ROOT_{size}={}", log.root().to_hex());
}
}