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}