code wiki / (root) / nx_trust_lib.nx

nx_trust_lib.nx source

↩ module page · 135 lines · 5453 B

1// nx_trust_lib.nx -- Nishi Office: client TRUST ACCOUNTING (F506, IOLTA / Utah RPC 1.15). 2// Append-only immutable trust ledger on the CID plane (history sacred = what bar audits want). 3// RPC 1.15 by construction: NO-COMMINGLING (per-client balances isolated), NO-OVERDRAW (a 4// disbursement exceeding a client's trust balance is REFUSED), THREE-WAY RECONCILIATION 5// (sum of client ledgers == trust account total; leg 3 = the bank statement a human matches). 6// Money = integer CENTS, no float (exact). DRY: reuses nx_matter_lib helpers. LIB (no main). 7// license_tier: ORIGINAL 8 9import "nx_matter_lib.nx" 10 11// parse a decimal string to i64 (cents) 12func tr_atoi(s: *u8) -> i64 { 13 var v: i64 = 0 14 var i: i64 = 0 15 while s[i] != (0 as u8) { 16 if s[i] >= 48 as u8 { if s[i] <= 57 as u8 { v = v * 10 + (s[i] - 48) } } 17 i = i + 1 18 } 19 return v 20} 21 22// format cents as "$X.XX", and "-$X.XX" when negative. 23// ★NEGATIVES, 2026-08-15 (found by nx_itoaclone). This is CLIENT TRUST ACCOUNTING, where a negative is 24// semantically real -- an overdraw or a deficit -- and it produced GARBAGE, not merely a wrong number: 25// cents = -1050 -> c = cents % 100 = -50, so (48 + (c / 10)) = 48 + (-5) = 43 = '+' 26// i.e. it rendered "$-10.+0". A NON-DIGIT BYTE INSIDE A MONEY STRING IS WORSE THAN A BLANK FIELD, 27// because it still LOOKS like a formatted amount and will be read as one. 28// ★★AND THE SIGN COULD VANISH ENTIRELY: at -50 cents the dollar part is 0, which prints as "0" with no 29// minus, so "$0.50" -- a DEBIT rendered as a CREDIT. That is the failure direction that silently 30// reverses the meaning of a ledger row, so the sign is hoisted OUT of the dollar part and emitted from 31// the SIGN OF THE WHOLE VALUE, before any division can discard it. 32// Positive and zero output is BYTE-IDENTICAL to before ("$10.50"), so no existing record changes. 33func tr_dollars(cents: i64, out: *u8) -> i64 { 34 var o: i64 = 0 35 var v: i64 = cents 36 if v < 0 { 37 out[o] = 45 as u8 38 o = o + 1 39 v = 0 - v 40 } 41 out[o] = 36 as u8 42 o = o + 1 43 o = mt_catn(out, o, v / 100) 44 out[o] = 46 as u8 45 o = o + 1 46 let c: i64 = v % 100 47 out[o] = (48 + (c / 10)) as u8 48 o = o + 1 49 out[o] = (48 + (c % 10)) as u8 50 o = o + 1 51 out[o] = 0 as u8 52 return o 53} 54 55// append a trust ledger entry (deposit|disburse) for a client; key "<client>:<seq>". 56func trust_put(prefix: *u8, client: *u8, seq: i64, matter: *u8, ttype: *u8, cents: i64) -> i64 { 57 let id: *u8 = sys_mmap(128) 58 var o: i64 = mt_catcopy(id, 0, client) 59 id[o] = 58 as u8 60 o = o + 1 61 o = mt_catn(id, o, seq) 62 id[o] = 0 as u8 63 let amt: *u8 = sys_mmap(32) 64 var ao: i64 = mt_catn(amt, 0, cents) 65 amt[ao] = 0 as u8 66 let k: *i64 = sys_mmap(8 * 4) as *i64 67 let v: *i64 = sys_mmap(8 * 4) as *i64 68 k[0] = ("amount" as *u8) as i64 69 v[0] = amt as i64 70 k[1] = ("client" as *u8) as i64 71 v[1] = client as i64 72 k[2] = ("matter" as *u8) as i64 73 v[2] = matter as i64 74 k[3] = ("type" as *u8) as i64 75 v[3] = ttype as i64 76 let rec: *u8 = sys_mmap(1024) 77 let rl: i64 = canon_encode(k, v, 4, rec) 78 return reg_put(prefix, "trust:" as *u8, "trust:__idx__" as *u8, id, rec, rl) 79} 80 81// a client's trust balance in cents (sum deposits - disbursements) -- per-client, no commingling. 82func trust_balance(prefix: *u8, client: *u8) -> i64 { 83 // SIZE-TO-NEED: buffer derived from the index itself. The pair this replaces 84 // returned -1 once the index passed 1 MiB, and -1 skips the walk below -- 85 // a trust account would have reported a ZERO BALANCE that looks like data. 86 let idxbox: *i64 = sys_mmap(16) as *i64 87 let ilen: i64 = reg_index_read(prefix, "trust:__idx__" as *u8, idxbox) 88 let idx: *u8 = idxbox[0] as *u8 89 let cp: *u8 = sys_mmap(128) 90 var cpl: i64 = mt_catcopy(cp, 0, client) 91 cp[cpl] = 58 as u8 92 cpl = cpl + 1 93 cp[cpl] = 0 as u8 94 let idb: *u8 = sys_mmap(256) 95 let po: *i64 = sys_mmap(16) as *i64 96 let lo: *i64 = sys_mmap(16) as *i64 97 let af: *u8 = sys_mmap(64) 98 let tf: *u8 = sys_mmap(32) 99 var bal: i64 = 0 100 var ls: i64 = 0 101 var i: i64 = 0 102 while i <= ilen { 103 var eol: i64 = 0 104 if i == ilen { eol = 1 } 105 if i < ilen { if idx[i] == 10 as u8 { eol = 1 } } 106 if eol == 1 { 107 if i > ls { 108 var c: i64 = 0 109 while ls + c < i { idb[c] = idx[ls + c]; c = c + 1 } 110 idb[c] = 0 as u8 111 var mtch: i64 = 1 112 var x: i64 = 0 113 while cp[x] != (0 as u8) { if idb[x] != cp[x] { mtch = 0 } x = x + 1 } 114 if mtch == 1 { 115 if reg_get(prefix, "trust:" as *u8, idb, po, lo) == 1 { 116 mt_field(po[0] as *u8, lo[0], "amount" as *u8, 6, af) 117 mt_field(po[0] as *u8, lo[0], "type" as *u8, 4, tf) 118 let amt: i64 = tr_atoi(af) 119 if mt_streq(tf, "deposit" as *u8) == 1 { bal = bal + amt } 120 if mt_streq(tf, "disburse" as *u8) == 1 { bal = bal - amt } 121 } 122 } 123 } 124 ls = i + 1 125 } 126 i = i + 1 127 } 128 return bal 129} 130 131// RPC 1.15 no-overdraw gate: 1 if client can disburse `cents`, else 0. 132func trust_disburse_ok(prefix: *u8, client: *u8, cents: i64) -> i64 { 133 if trust_balance(prefix, client) >= cents { return 1 } 134 return 0 135}