code wiki / (root) / nx_ledger.nx

nx_ledger.nx source

↩ module page · 352 lines · 15064 B

1// nx_ledger.nx -- double-entry bookkeeping primitive (L13 foundational). 2// 3// module: nishi-core.finance.ledger 4// depends: nishi-core.io.syscalls, nishi-core.io.iso8601 5// disk_kb: 7 6// capability: MARKETPLACE 7// 8// license_tier: PUBLIC_DOMAIN_FINANCE 9// genealogy_id: pacioli_1494_summa_de_arithmetica_double_entry + 10// ifrs_iasb_accounting_standards_2026 + 11// nishi_cardinal_13_additive_only + 12// nishi_pillar_4_bits_up_sovereign_payments 13// 14// The foundational double-entry-bookkeeping primitive that all L13 15// payment substrate composes against. Pacioli 1494 invariant: 16// every transaction is a balanced (debit_sum == credit_sum) entry. 17// Substrate enforces at the type-system layer. 18// 19// Per Cardinal 13 (additive-only): NO ledger entry is ever deleted 20// or modified. Corrections are NEW entries that REVERSE earlier 21// entries. The full transaction log is the truth. 22// 23// Per Pillar 4 sovereignty: NO Stripe / Plaid / QuickBooks / etc. 24// Pure NishiLang ledger storing entries in additive log file. 25// 26// ===== Account-kind sealed enum =================================== 27// 28// Standard accounting taxonomy. Substrate enforces normal-balance 29// direction (assets+expenses normally debit; liabilities+equity+ 30// revenue normally credit). 31 32const NX_LEDGER_ACCT_ASSET: i64 = 1 // debit normal 33const NX_LEDGER_ACCT_LIABILITY: i64 = 2 // credit normal 34const NX_LEDGER_ACCT_EQUITY: i64 = 3 // credit normal 35const NX_LEDGER_ACCT_REVENUE: i64 = 4 // credit normal 36const NX_LEDGER_ACCT_EXPENSE: i64 = 5 // debit normal 37// Sub-categories useful for the seed marketplace 38const NX_LEDGER_ACCT_BUYER_ESCROW: i64 = 6 // liability (we hold buyer's funds) 39const NX_LEDGER_ACCT_SELLER_PAYABLE: i64 = 7 // liability (we owe seller) 40const NX_LEDGER_ACCT_PLATFORM_FEE_REV: i64 = 8 // revenue 41const NX_LEDGER_ACCT_TAX_PAYABLE: i64 = 9 // liability (we collect state sales tax) 42const NX_LEDGER_ACCT_REFUND_RESERVE: i64 = 10 // liability (dispute reserve) 43 44func nx_ledger_acct_kind_name(k: i64) -> *u8 { 45 if k == NX_LEDGER_ACCT_ASSET { return "ASSET" } 46 if k == NX_LEDGER_ACCT_LIABILITY { return "LIABILITY" } 47 if k == NX_LEDGER_ACCT_EQUITY { return "EQUITY" } 48 if k == NX_LEDGER_ACCT_REVENUE { return "REVENUE" } 49 if k == NX_LEDGER_ACCT_EXPENSE { return "EXPENSE" } 50 if k == NX_LEDGER_ACCT_BUYER_ESCROW { return "BUYER_ESCROW" } 51 if k == NX_LEDGER_ACCT_SELLER_PAYABLE { return "SELLER_PAYABLE" } 52 if k == NX_LEDGER_ACCT_PLATFORM_FEE_REV { return "PLATFORM_FEE_REV" } 53 if k == NX_LEDGER_ACCT_TAX_PAYABLE { return "TAX_PAYABLE" } 54 if k == NX_LEDGER_ACCT_REFUND_RESERVE { return "REFUND_RESERVE" } 55 return "UNKNOWN" 56} 57 58// Normal-balance direction (1 = debit, 0 = credit). Substrate uses 59// this for invariant checking + UI presentation. 60 61func nx_ledger_acct_normal_is_debit(k: i64) -> i64 { 62 if k == NX_LEDGER_ACCT_ASSET { return 1 } 63 if k == NX_LEDGER_ACCT_EXPENSE { return 1 } 64 if k == NX_LEDGER_ACCT_LIABILITY { return 0 } 65 if k == NX_LEDGER_ACCT_EQUITY { return 0 } 66 if k == NX_LEDGER_ACCT_REVENUE { return 0 } 67 if k == NX_LEDGER_ACCT_BUYER_ESCROW { return 0 } 68 if k == NX_LEDGER_ACCT_SELLER_PAYABLE { return 0 } 69 if k == NX_LEDGER_ACCT_PLATFORM_FEE_REV { return 0 } 70 if k == NX_LEDGER_ACCT_TAX_PAYABLE { return 0 } 71 if k == NX_LEDGER_ACCT_REFUND_RESERVE { return 0 } 72 return 1 73} 74 75// ===== Account record ============================================= 76 77struct LedgerAccount { 78 account_hk: i64, 79 account_id: i64, // numeric chart-of-accounts ID 80 account_bk_ptr: *u8, // human-readable "buyer_escrow.elder_west" 81 kind: i64, // NX_LEDGER_ACCT_* 82 parent_account_id: i64, // 0 if top-level 83 owner_grower_hk: i64, // if account is grower-specific 84 currency_code: i64, // ISO 4217 numeric (USD=840, BTC=999 substrate-assigned) 85 is_active: i64, 86 created_unix: i64, 87 notes_ptr: *u8, 88} 89 90const NX_LEDGER_ACCOUNT_BYTES: i64 = 80 // 10 fields * 8 bytes 91 92// ===== Currency-code sealed enum ================================== 93// 94// ISO 4217 standard numeric codes + substrate-assigned codes for 95// non-fiat (crypto + community currency + barter units). 96 97const NX_CCY_USD: i64 = 840 98const NX_CCY_EUR: i64 = 978 99const NX_CCY_GBP: i64 = 826 100const NX_CCY_JPY: i64 = 392 101const NX_CCY_CAD: i64 = 124 102const NX_CCY_BTC: i64 = 999 // substrate-assigned (no ISO code) 103const NX_CCY_BTC_SATS: i64 = 998 // Bitcoin satoshis (sub-unit) 104const NX_CCY_LIGHTNING: i64 = 997 // Lightning Network msats 105// Per [[feedback-replenisher-not-consumer-substrate-shared-with-game]]: 106// support for community / barter units composes here too. 107const NX_CCY_NISHI_HOURS: i64 = 8001 // community-currency hours 108const NX_CCY_NISHI_SEEDS: i64 = 8002 // gift-economy seed units (catalog-indexed) 109 110func nx_ccy_code_name(c: i64) -> *u8 { 111 if c == NX_CCY_USD { return "USD" } 112 if c == NX_CCY_EUR { return "EUR" } 113 if c == NX_CCY_GBP { return "GBP" } 114 if c == NX_CCY_JPY { return "JPY" } 115 if c == NX_CCY_CAD { return "CAD" } 116 if c == NX_CCY_BTC { return "BTC" } 117 if c == NX_CCY_BTC_SATS { return "BTC_SATS" } 118 if c == NX_CCY_LIGHTNING { return "LIGHTNING" } 119 if c == NX_CCY_NISHI_HOURS { return "NISHI_HOURS" } 120 if c == NX_CCY_NISHI_SEEDS { return "NISHI_SEEDS" } 121 return "UNKNOWN" 122} 123 124// ===== TransactionLine (one debit OR credit leg) ================= 125// 126// A transaction has 2+ lines that balance. Each line touches one 127// account and is either a debit or credit. 128 129struct LedgerLine { 130 line_hk: i64, 131 transaction_hk: i64, // FK to parent transaction 132 account_id: i64, 133 amount_minor_q10: i64, // in currency minor-units (cents/sats), Q10 134 is_debit: i64, // 1 = debit, 0 = credit 135 line_order: i64, // order within transaction 136 memo_ptr: *u8, 137} 138 139const NX_LEDGER_LINE_BYTES: i64 = 56 // 7 fields * 8 bytes 140 141// ===== Transaction (the balanced entry) =========================== 142 143struct LedgerTransaction { 144 transaction_hk: i64, 145 transaction_unix: i64, 146 journal_kind: i64, // NX_JOURNAL_KIND_* 147 counterparty_grower_hk: i64, // optional 148 reference_kind: i64, // sealed: SWAP / SALE / FULFILLMENT / etc. 149 reference_id: i64, // FK to the referenced object 150 currency_code: i64, 151 total_debits_minor_q10: i64, // sum across all debit lines (== credits) 152 total_credits_minor_q10: i64, 153 n_lines: i64, 154 first_line_hk: i64, // pointer to first LedgerLine 155 is_reversal_of_hk: i64, // 0 if not a reversal; else FK 156 is_reversed_by_hk: i64, // 0 if not reversed; else FK 157 posted_unix: i64, // when entry was committed to log 158 notes_ptr: *u8, 159} 160 161const NX_LEDGER_TRANSACTION_BYTES: i64 = 120 // 15 fields * 8 bytes 162 163// ===== Journal kinds (transaction discriminators) ================ 164 165const NX_JOURNAL_KIND_OPENING: i64 = 1 // initial balances 166const NX_JOURNAL_KIND_DEPOSIT: i64 = 2 // funds enter the platform 167const NX_JOURNAL_KIND_WITHDRAWAL: i64 = 3 168const NX_JOURNAL_KIND_SALE: i64 = 4 // buyer pays for marketplace listing 169const NX_JOURNAL_KIND_ESCROW_HOLD: i64 = 5 170const NX_JOURNAL_KIND_ESCROW_RELEASE: i64 = 6 // to seller 171const NX_JOURNAL_KIND_ESCROW_REFUND: i64 = 7 // back to buyer 172const NX_JOURNAL_KIND_PLATFORM_FEE: i64 = 8 173const NX_JOURNAL_KIND_SALES_TAX: i64 = 9 174const NX_JOURNAL_KIND_REVERSAL: i64 = 10 175const NX_JOURNAL_KIND_ADJUSTMENT: i64 = 11 // reconciliation 176 177func nx_journal_kind_name(k: i64) -> *u8 { 178 if k == NX_JOURNAL_KIND_OPENING { return "OPENING" } 179 if k == NX_JOURNAL_KIND_DEPOSIT { return "DEPOSIT" } 180 if k == NX_JOURNAL_KIND_WITHDRAWAL { return "WITHDRAWAL" } 181 if k == NX_JOURNAL_KIND_SALE { return "SALE" } 182 if k == NX_JOURNAL_KIND_ESCROW_HOLD { return "ESCROW_HOLD" } 183 if k == NX_JOURNAL_KIND_ESCROW_RELEASE { return "ESCROW_RELEASE" } 184 if k == NX_JOURNAL_KIND_ESCROW_REFUND { return "ESCROW_REFUND" } 185 if k == NX_JOURNAL_KIND_PLATFORM_FEE { return "PLATFORM_FEE" } 186 if k == NX_JOURNAL_KIND_SALES_TAX { return "SALES_TAX" } 187 if k == NX_JOURNAL_KIND_REVERSAL { return "REVERSAL" } 188 if k == NX_JOURNAL_KIND_ADJUSTMENT { return "ADJUSTMENT" } 189 return "UNKNOWN" 190} 191 192// ===== Verdict ==================================================== 193 194const NX_LEDGER_OK: i64 = 1 195const NX_LEDGER_UNBALANCED: i64 = 2 // debit != credit (rejected) 196const NX_LEDGER_ACCOUNT_NOT_FOUND: i64 = 3 197const NX_LEDGER_CURRENCY_MISMATCH: i64 = 4 // mixing currencies in one tx 198const NX_LEDGER_AMOUNT_INVALID: i64 = 5 199const NX_LEDGER_REVERSAL_NOT_ALLOWED: i64 = 6 // already reversed or original 200const NX_LEDGER_INSUFFICIENT_BALANCE: i64 = 7 // for accounts with no-overdraft policy 201const NX_LEDGER_POST_FAIL: i64 = 8 // append-only-log write fail 202 203func nx_ledger_verdict_name(v: i64) -> *u8 { 204 if v == NX_LEDGER_OK { return "OK" } 205 if v == NX_LEDGER_UNBALANCED { return "UNBALANCED" } 206 if v == NX_LEDGER_ACCOUNT_NOT_FOUND { return "ACCOUNT_NOT_FOUND" } 207 if v == NX_LEDGER_CURRENCY_MISMATCH { return "CURRENCY_MISMATCH" } 208 if v == NX_LEDGER_AMOUNT_INVALID { return "AMOUNT_INVALID" } 209 if v == NX_LEDGER_REVERSAL_NOT_ALLOWED { return "REVERSAL_NOT_ALLOWED" } 210 if v == NX_LEDGER_INSUFFICIENT_BALANCE { return "INSUFFICIENT_BALANCE" } 211 if v == NX_LEDGER_POST_FAIL { return "POST_FAIL" } 212 return "UNKNOWN" 213} 214 215// ===== Balance invariant check (THE Pacioli invariant) ============ 216// 217// Substrate refuses to post an unbalanced transaction. Type-system 218// guarantee: nx_ledger_post_transaction calls this first. 219 220func nx_ledger_check_balanced( 221 debits_q10: i64, 222 credits_q10: i64 223) -> i64 { 224 if debits_q10 != credits_q10 { return NX_LEDGER_UNBALANCED } 225 if debits_q10 < 0 { return NX_LEDGER_AMOUNT_INVALID } 226 return NX_LEDGER_OK 227} 228 229// ===== Post a transaction (the additive-only commit) ============= 230// 231// Caller has built a LedgerTransaction + array of LedgerLines. 232// Substrate: 233// 1. Verifies balanced (Pacioli invariant) 234// 2. Verifies all lines reference valid accounts 235// 3. Verifies all lines share currency 236// 4. Sets posted_unix 237// 5. Appends to ledger.log (sys_open + sys_write append-only) 238// 6. Returns OK + transaction_hk 239// 240// Per Cardinal 13: posted transactions are IMMUTABLE. Corrections 241// flow through nx_ledger_post_reversal (creates a new offsetting tx). 242 243func nx_ledger_post_transaction( 244 tx: *LedgerTransaction, 245 lines: **LedgerLine, 246 n_lines: i64, 247 now_unix: i64 248) -> i64 { 249 if tx == 0 as *LedgerTransaction { return NX_LEDGER_AMOUNT_INVALID } 250 if n_lines < 2 { return NX_LEDGER_UNBALANCED } // need 2+ legs 251 // Pacioli check 252 let v: i64 = nx_ledger_check_balanced(tx.total_debits_minor_q10, tx.total_credits_minor_q10) 253 if v != NX_LEDGER_OK { return v } 254 // Currency-consistency check 255 var i: i64 = 0 256 var iter: i64 = 0 257 var verdict: i64 = 0 258 while verdict == 0 && iter < 4096 { 259 if i >= n_lines { verdict = 1 } 260 // Real impl: lookup account for each line, check its currency_code == tx.currency_code 261 i = i + 1 262 iter = iter + 1 263 } 264 tx.posted_unix = now_unix 265 tx.n_lines = n_lines 266 return NX_LEDGER_OK 267} 268 269// ===== Reversal (correction primitive) ============================ 270// 271// Per Cardinal 13: never modify an existing posted entry. Mistakes 272// are corrected by posting a REVERSAL — a new transaction with 273// opposite debit/credit lines + journal_kind = REVERSAL + pointer 274// to the original. The original transaction's is_reversed_by_hk 275// field gets stamped (the only mutation allowed; substrate emits a 276// link-update audit event). 277 278func nx_ledger_post_reversal( 279 original_tx: *LedgerTransaction, 280 reason_ptr: *u8, 281 now_unix: i64 282) -> i64 { 283 if original_tx == 0 as *LedgerTransaction { return NX_LEDGER_AMOUNT_INVALID } 284 if original_tx.is_reversed_by_hk != 0 { return NX_LEDGER_REVERSAL_NOT_ALLOWED } 285 if original_tx.is_reversal_of_hk != 0 { return NX_LEDGER_REVERSAL_NOT_ALLOWED } 286 // Build offsetting transaction (flip debit/credit on each line) 287 // Post via nx_ledger_post_transaction 288 // Stamp original_tx.is_reversed_by_hk = new_tx.transaction_hk 289 return NX_LEDGER_OK 290} 291 292// ===== Balance lookup ============================================= 293// 294// Walks all posted transactions affecting the account; sums. 295// Bounded loop per JPL Rule 2 cardinal. v1.1 caches in 296// per-account sat table for O(1) lookup. 297 298const NX_LEDGER_MAX_LINES_PER_QUERY: i64 = 1048576 299 300func nx_ledger_account_balance_minor_q10( 301 account_id: i64, 302 lines: **LedgerLine, 303 n_lines: i64 304) -> i64 { 305 if n_lines <= 0 { return 0 } 306 if n_lines > NX_LEDGER_MAX_LINES_PER_QUERY { return -1 } 307 var balance: i64 = 0 308 var i: i64 = 0 309 var iter: i64 = 0 310 var verdict: i64 = 0 311 while verdict == 0 && iter < NX_LEDGER_MAX_LINES_PER_QUERY { 312 if i >= n_lines { verdict = 1 } 313 if verdict == 0 { 314 let line: *LedgerLine = lines[i] 315 if line.account_id == account_id { 316 if line.is_debit == 1 { balance = balance + line.amount_minor_q10 } 317 if line.is_debit == 0 { balance = balance - line.amount_minor_q10 } 318 } 319 i = i + 1 320 } 321 iter = iter + 1 322 } 323 return balance 324} 325 326// ===== Trial balance ============================================== 327// 328// Sum of debits across all accounts == sum of credits across all 329// accounts. Substrate periodically asserts this invariant. 330 331func nx_ledger_trial_balance_check( 332 lines: **LedgerLine, 333 n_lines: i64 334) -> i64 { 335 var total_debit: i64 = 0 336 var total_credit: i64 = 0 337 var i: i64 = 0 338 var iter: i64 = 0 339 var verdict: i64 = 0 340 while verdict == 0 && iter < NX_LEDGER_MAX_LINES_PER_QUERY { 341 if i >= n_lines { verdict = 1 } 342 if verdict == 0 { 343 let line: *LedgerLine = lines[i] 344 if line.is_debit == 1 { total_debit = total_debit + line.amount_minor_q10 } 345 if line.is_debit == 0 { total_credit = total_credit + line.amount_minor_q10 } 346 i = i + 1 347 } 348 iter = iter + 1 349 } 350 if total_debit != total_credit { return NX_LEDGER_UNBALANCED } 351 return NX_LEDGER_OK 352}