nx_iso20022_lib.nx source
↩ module page · 163 lines · 6398 B
1// nx_iso20022_lib.nx -- F989: ISO 20022 payment-message parse + integrity validation.
2//
3// 2026 is the inflection: Fedwire migrated to ISO 20022 in July 2025 and Swift ends MT coexistence in
4// November. A pain.001 (customer credit-transfer initiation) carries its OWN integrity fields -- NbOfTxs
5// (the declared transaction count) and CtrlSum (the declared total). The sovereign property here is that
6// these are CHECKED against the actual message content, never trusted: a corrupted or TAMPERED payment
7// file -- one where an amount was altered but the control sum was not -- is REJECTED by construction.
8// Money is integer minor units; a control sum is compared to the exact cent, no float, no tolerance.
9//
10// FAIL-CLOSED: a missing required field (NbOfTxs / CtrlSum) is a rejection, not a default -- a payment
11// message you cannot validate is a payment message you do not process.
12//
13// SCOPE (declared, honest): this parses element text and validates the two internal-integrity invariants.
14// Full XSD schema validation, namespace resolution, and variable-fraction-digit currencies (this assumes
15// 2 fraction digits, correct for USD/EUR/most) are DECLARED follow-ons, not silently handled.
16// DRY: composes nx_matter_lib (sys_*/mt_*). A validated message feeds nx_ledger as transfers. license_tier: ORIGINAL LIB.
17
18import "nx_matter_lib.nx"
19
20const ISO_OK: i64 = 0
21const ISO_NO_FIELD: i64 = 0 - 1 // a required field is absent (malformed)
22const ISO_TX_MISMATCH: i64 = 0 - 2 // NbOfTxs != actual CdtTrfTxInf count
23const ISO_NO_CTRL: i64 = 0 - 3 // CtrlSum absent
24const ISO_SUM_MISMATCH: i64 = 0 - 4 // CtrlSum != actual sum of amounts (tamper/corruption)
25
26func iso_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
27
28// substring search from `from`; returns index or -1. Mismatch jumps the cursor past the run (no break).
29func iso_find(hay: *u8, hl: i64, needle: *u8, nl: i64, from: i64) -> i64 {
30 if nl == 0 { return 0 - 1 }
31 var i: i64 = from
32 while i + nl <= hl {
33 var j: i64 = 0
34 while j < nl {
35 if hay[i + j] == needle[j] { j = j + 1 } else { j = nl + 1 }
36 }
37 if j == nl { return i }
38 i = i + 1
39 }
40 return 0 - 1
41}
42
43// build "<tag>" -> out, returns len
44func iso_open(tag: *u8, out: *u8) -> i64 {
45 out[0] = 60 as u8
46 var o: i64 = 1
47 var i: i64 = 0
48 while tag[i] != (0 as u8) { out[o] = tag[i]; o = o + 1; i = i + 1 }
49 out[o] = 62 as u8
50 o = o + 1
51 out[o] = 0 as u8
52 return o
53}
54// build "</tag>" -> out, returns len
55func iso_close(tag: *u8, out: *u8) -> i64 {
56 out[0] = 60 as u8
57 out[1] = 47 as u8
58 var o: i64 = 2
59 var i: i64 = 0
60 while tag[i] != (0 as u8) { out[o] = tag[i]; o = o + 1; i = i + 1 }
61 out[o] = 62 as u8
62 o = o + 1
63 out[o] = 0 as u8
64 return o
65}
66
67// extract the text content of the FIRST <tag>...</tag> into out; returns length, -1 if not found.
68func iso_element(xml: *u8, xl: i64, tag: *u8, out: *u8) -> i64 {
69 let ot: *u8 = sys_mmap(128)
70 let ct: *u8 = sys_mmap(128)
71 let ol: i64 = iso_open(tag, ot)
72 let cl: i64 = iso_close(tag, ct)
73 let os: i64 = iso_find(xml, xl, ot, ol, 0)
74 if os < 0 { return 0 - 1 }
75 let cs: i64 = os + ol
76 let ce: i64 = iso_find(xml, xl, ct, cl, cs)
77 if ce < 0 { return 0 - 1 }
78 var t: i64 = 0
79 var p: i64 = cs
80 while p < ce { out[t] = xml[p]; t = t + 1; p = p + 1 }
81 out[t] = 0 as u8
82 return t
83}
84
85// count occurrences of the opening <tag>.
86func iso_count(xml: *u8, xl: i64, tag: *u8) -> i64 {
87 let ot: *u8 = sys_mmap(128)
88 let ol: i64 = iso_open(tag, ot)
89 var c: i64 = 0
90 var from: i64 = 0
91 var go: i64 = 1
92 while go == 1 {
93 let p: i64 = iso_find(xml, xl, ot, ol, from)
94 if p < 0 { go = 0 } else { c = c + 1; from = p + ol }
95 }
96 return c
97}
98
99// parse a decimal amount string "1234.56" -> 123456 minor units (2 fraction digits assumed).
100func iso_amount_to_cents(s: *u8) -> i64 {
101 var slen: i64 = 0
102 var dot: i64 = 0 - 1
103 while s[slen] != (0 as u8) { if s[slen] == (46 as u8) { if dot < 0 { dot = slen } } slen = slen + 1 }
104 var wend: i64 = slen
105 if dot >= 0 { wend = dot }
106 var whole: i64 = 0
107 var p: i64 = 0
108 while p < wend { whole = whole * 10 + (s[p] - 48); p = p + 1 }
109 var cents: i64 = 0
110 var fd: i64 = 0
111 if dot >= 0 {
112 var q: i64 = dot + 1
113 while q < slen { if fd < 2 { cents = cents * 10 + (s[q] - 48); fd = fd + 1 } q = q + 1 }
114 }
115 while fd < 2 { cents = cents * 10; fd = fd + 1 }
116 return whole * 100 + cents
117}
118
119// sum every <tag>...</tag> amount (in minor units) across the message.
120func iso_sum_amounts(xml: *u8, xl: i64, tag: *u8) -> i64 {
121 let ot: *u8 = sys_mmap(128)
122 let ct: *u8 = sys_mmap(128)
123 let ol: i64 = iso_open(tag, ot)
124 let cl: i64 = iso_close(tag, ct)
125 let val: *u8 = sys_mmap(64)
126 var sum: i64 = 0
127 var from: i64 = 0
128 var go: i64 = 1
129 while go == 1 {
130 let os: i64 = iso_find(xml, xl, ot, ol, from)
131 if os < 0 { go = 0 } else {
132 let cs: i64 = os + ol
133 let ce: i64 = iso_find(xml, xl, ct, cl, cs)
134 if ce < 0 { go = 0 } else {
135 var t: i64 = 0
136 var q: i64 = cs
137 while q < ce { if t < 63 { val[t] = xml[q]; t = t + 1 } q = q + 1 }
138 val[t] = 0 as u8
139 sum = sum + iso_amount_to_cents(val)
140 from = ce + cl
141 }
142 }
143 }
144 return sum
145}
146
147// ★VALIDATE the message's own integrity. 0 = valid; negative code = the specific failure.
148// NbOfTxs must equal the actual CdtTrfTxInf count; CtrlSum must equal the actual sum of InstdAmt.
149func iso_validate(xml: *u8, xl: i64) -> i64 {
150 let nb: *u8 = sys_mmap(32)
151 if iso_element(xml, xl, "NbOfTxs" as *u8, nb) < 0 { return ISO_NO_FIELD }
152 var declared_n: i64 = 0
153 var i: i64 = 0
154 while nb[i] != (0 as u8) { declared_n = declared_n * 10 + (nb[i] - 48); i = i + 1 }
155 let actual_n: i64 = iso_count(xml, xl, "CdtTrfTxInf" as *u8)
156 if declared_n != actual_n { return ISO_TX_MISMATCH }
157 let cs: *u8 = sys_mmap(32)
158 if iso_element(xml, xl, "CtrlSum" as *u8, cs) < 0 { return ISO_NO_CTRL }
159 let declared_sum: i64 = iso_amount_to_cents(cs)
160 let actual_sum: i64 = iso_sum_amounts(xml, xl, "InstdAmt" as *u8)
161 if declared_sum != actual_sum { return ISO_SUM_MISMATCH }
162 return ISO_OK
163}