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}