code wiki / (root) / nx_escrow_hold.nx

nx_escrow_hold.nx source

↩ module page · 365 lines · 16258 B

1// nx_escrow_hold.nx -- multi-party escrow primitive composing nx_ledger. 2// 3// module: nishi-core.finance.escrow_hold 4// depends: nishi-core.finance.ledger, nishi-core.io.syscalls, 5// nishi-core.io.iso8601 6// disk_kb: 6 7// capability: MARKETPLACE 8// 9// license_tier: PUBLIC_DOMAIN_FINANCE 10// genealogy_id: ucc_article_2_sale_of_goods + 11// escrow_hold_pattern_software_engineering + 12// nishi_pillar_4_sealed_swap_sale_separation_2026 13// 14// Multi-party escrow: buyer funds → platform-held escrow account → 15// released to seller on confirmed-delivery OR refunded to buyer on 16// dispute-tribunal favor. Composes against nx_ledger primitive 17// shipped same arc. 18// 19// Per Pillar 4 sealed-separation: this primitive references 20// nx_marketplace_listing (sale-class) ONLY. nx_swap_ledger (gift) 21// has no escrow concept — gifts don't escrow. Compile-time 22// guarantee: no codepath from nx_swap_ledger → escrow. 23// 24// ===== State machine ============================================== 25// 26// FUNDED buyer's payment processed; funds in buyer_escrow account 27// SHIP_PENDING waiting for seller to ship 28// SHIPPED seller marked shipped; tracking number received 29// DELIVERED carrier confirmed delivery 30// CONFIRMED buyer marked received-as-described 31// RELEASING release in progress (1-step write of release tx) 32// RELEASED funds settled to seller_payable account 33// DISPUTED buyer raised dispute; tribunal selecting jurors 34// REFUND_PENDING tribunal favored buyer; refund posting 35// REFUNDED funds settled back to buyer's source account 36// FROZEN regulatory hold (sanctions / fraud investigation); 37// no release in either direction 38// TIMED_OUT_TO_SELLER buyer never confirmed but delivery confirmed 39// + 14-day window passed; auto-release to seller 40 41// nx_safety_envelope: 42// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 43// sil_target: SIL1 44// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 45// verdict: NOT_YET_EVALUATED 46 47import "nx_syscalls.nx" 48import "nx_ledger.nx" 49 50// ===== EscrowState sealed enum ==================================== 51 52const NX_ESCROW_FUNDED: i64 = 1 53const NX_ESCROW_SHIP_PENDING: i64 = 2 54const NX_ESCROW_SHIPPED: i64 = 3 55const NX_ESCROW_DELIVERED: i64 = 4 56const NX_ESCROW_CONFIRMED: i64 = 5 57const NX_ESCROW_RELEASING: i64 = 6 58const NX_ESCROW_RELEASED: i64 = 7 59const NX_ESCROW_DISPUTED: i64 = 8 60const NX_ESCROW_REFUND_PENDING: i64 = 9 61const NX_ESCROW_REFUNDED: i64 = 10 62const NX_ESCROW_FROZEN: i64 = 11 63const NX_ESCROW_TIMED_OUT_TO_SELLER: i64 = 12 64 65func nx_escrow_state_name(s: i64) -> *u8 { 66 if s == NX_ESCROW_FUNDED { return "FUNDED" } 67 if s == NX_ESCROW_SHIP_PENDING { return "SHIP_PENDING" } 68 if s == NX_ESCROW_SHIPPED { return "SHIPPED" } 69 if s == NX_ESCROW_DELIVERED { return "DELIVERED" } 70 if s == NX_ESCROW_CONFIRMED { return "CONFIRMED" } 71 if s == NX_ESCROW_RELEASING { return "RELEASING" } 72 if s == NX_ESCROW_RELEASED { return "RELEASED" } 73 if s == NX_ESCROW_DISPUTED { return "DISPUTED" } 74 if s == NX_ESCROW_REFUND_PENDING { return "REFUND_PENDING" } 75 if s == NX_ESCROW_REFUNDED { return "REFUNDED" } 76 if s == NX_ESCROW_FROZEN { return "FROZEN" } 77 if s == NX_ESCROW_TIMED_OUT_TO_SELLER { return "TIMED_OUT_TO_SELLER" } 78 return "UNKNOWN" 79} 80 81// ===== Verdict ==================================================== 82 83const NX_ESCROW_OK: i64 = 1 84const NX_ESCROW_NOT_FOUND: i64 = 2 85const NX_ESCROW_INVALID_TRANSITION: i64 = 3 86const NX_ESCROW_INSUFFICIENT_FUNDS: i64 = 4 87const NX_ESCROW_LEDGER_POST_FAIL: i64 = 5 88const NX_ESCROW_TIMEOUT_NOT_REACHED: i64 = 6 89const NX_ESCROW_FROZEN_NO_OP: i64 = 7 90const NX_ESCROW_DISPUTE_TRIBUNAL_REQUIRED: i64 = 8 91 92func nx_escrow_verdict_name(v: i64) -> *u8 { 93 if v == NX_ESCROW_OK { return "OK" } 94 if v == NX_ESCROW_NOT_FOUND { return "NOT_FOUND" } 95 if v == NX_ESCROW_INVALID_TRANSITION { return "INVALID_TRANSITION" } 96 if v == NX_ESCROW_INSUFFICIENT_FUNDS { return "INSUFFICIENT_FUNDS" } 97 if v == NX_ESCROW_LEDGER_POST_FAIL { return "LEDGER_POST_FAIL" } 98 if v == NX_ESCROW_TIMEOUT_NOT_REACHED { return "TIMEOUT_NOT_REACHED" } 99 if v == NX_ESCROW_FROZEN_NO_OP { return "FROZEN_NO_OP" } 100 if v == NX_ESCROW_DISPUTE_TRIBUNAL_REQUIRED { return "DISPUTE_TRIBUNAL_REQUIRED" } 101 return "UNKNOWN" 102} 103 104// ===== Timing constants =========================================== 105 106const NX_ESCROW_SHIP_DEADLINE_DAYS: i64 = 7 // seller must ship within 7d of FUNDED 107const NX_ESCROW_AUTO_RELEASE_DAYS: i64 = 14 // post-DELIVERED auto-release window 108const NX_ESCROW_DISPUTE_WINDOW_DAYS: i64 = 30 // buyer can raise dispute within 30d 109const NX_ESCROW_FROZEN_REVIEW_DAYS: i64 = 60 // regulatory freeze max before manual review 110 111// ===== EscrowHold struct ========================================== 112 113struct EscrowHold { 114 escrow_hk: i64, 115 listing_hk: i64, // FK MarketplaceListing 116 order_hk: i64, // FK MarketplaceOrder (queued) 117 buyer_hk: i64, // FK hub_grower 118 seller_hk: i64, // FK hub_grower 119 // Amounts (currency-native minor units, Q10) 120 principal_minor_q10: i64, // listing price 121 platform_fee_minor_q10: i64, // our take rate 122 sales_tax_minor_q10: i64, // collected per state law 123 total_held_minor_q10: i64, // = principal + fee + tax 124 currency_code: i64, // ISO 4217 numeric per nx_ledger 125 // Ledger account references 126 buyer_escrow_account_id: i64, // liability we owe back 127 seller_payable_account_id: i64, // liability we owe seller post-release 128 platform_fee_revenue_account_id: i64, 129 tax_payable_account_id: i64, 130 // State machine 131 state: i64, // NX_ESCROW_* 132 state_ts_unix: i64, 133 // Lifecycle timestamps 134 funded_unix: i64, 135 ship_deadline_unix: i64, 136 shipped_unix: i64, 137 delivered_unix: i64, 138 confirmed_unix: i64, 139 auto_release_deadline_unix: i64, 140 released_unix: i64, 141 disputed_unix: i64, 142 dispute_tribunal_hk: i64, // FK to nx_dispute_tribunal 143 refunded_unix: i64, 144 // Ledger transaction refs 145 fund_transaction_hk: i64, // the deposit→escrow ledger entry 146 release_transaction_hk: i64, // 0 if not yet released 147 refund_transaction_hk: i64, // 0 if not refunded 148 is_current: i64, 149} 150 151const NX_ESCROW_HOLD_BYTES: i64 = 232 // 29 fields * 8 bytes 152 153// ===== Constructor (post-funding) ================================= 154// 155// Called by marketplace order primitive after buyer payment lands 156// in buyer_escrow ledger account. Composes one balanced ledger 157// transaction: debit-buyer-source / credit-buyer-escrow. 158 159func nx_escrow_hold_new( 160 listing_hk: i64, 161 order_hk: i64, 162 buyer_hk: i64, 163 seller_hk: i64, 164 principal_minor_q10: i64, 165 platform_fee_minor_q10: i64, 166 sales_tax_minor_q10: i64, 167 currency_code: i64, 168 buyer_escrow_account_id: i64, 169 seller_payable_account_id: i64, 170 platform_fee_revenue_account_id: i64, 171 tax_payable_account_id: i64, 172 now_unix: i64 173) -> *EscrowHold { 174 let raw: *u8 = sys_mmap(NX_ESCROW_HOLD_BYTES) 175 let h: *EscrowHold = raw as *EscrowHold 176 h.escrow_hk = 0 177 h.listing_hk = listing_hk 178 h.order_hk = order_hk 179 h.buyer_hk = buyer_hk 180 h.seller_hk = seller_hk 181 h.principal_minor_q10 = principal_minor_q10 182 h.platform_fee_minor_q10 = platform_fee_minor_q10 183 h.sales_tax_minor_q10 = sales_tax_minor_q10 184 h.total_held_minor_q10 = principal_minor_q10 + platform_fee_minor_q10 + sales_tax_minor_q10 185 h.currency_code = currency_code 186 h.buyer_escrow_account_id = buyer_escrow_account_id 187 h.seller_payable_account_id = seller_payable_account_id 188 h.platform_fee_revenue_account_id = platform_fee_revenue_account_id 189 h.tax_payable_account_id = tax_payable_account_id 190 h.state = NX_ESCROW_FUNDED 191 h.state_ts_unix = now_unix 192 h.funded_unix = now_unix 193 h.ship_deadline_unix = now_unix + (NX_ESCROW_SHIP_DEADLINE_DAYS * 86400) 194 h.shipped_unix = 0 195 h.delivered_unix = 0 196 h.confirmed_unix = 0 197 h.auto_release_deadline_unix = 0 198 h.released_unix = 0 199 h.disputed_unix = 0 200 h.dispute_tribunal_hk = 0 201 h.refunded_unix = 0 202 h.fund_transaction_hk = 0 203 h.release_transaction_hk = 0 204 h.refund_transaction_hk = 0 205 h.is_current = 1 206 return h 207} 208 209// ===== State transition guard ===================================== 210// 211// Sealed lifecycle; substrate refuses backward + invalid transitions. 212 213func nx_escrow_can_transition(current: i64, target: i64) -> i64 { 214 // Absorbing states 215 if current == NX_ESCROW_RELEASED { return 0 } 216 if current == NX_ESCROW_REFUNDED { return 0 } 217 // FROZEN is reachable from anything (regulatory) 218 if target == NX_ESCROW_FROZEN { return 1 } 219 // From FROZEN only DISPUTED / REFUND_PENDING / RELEASING (post-review) 220 if current == NX_ESCROW_FROZEN { 221 if target == NX_ESCROW_DISPUTED { return 1 } 222 if target == NX_ESCROW_REFUND_PENDING { return 1 } 223 if target == NX_ESCROW_RELEASING { return 1 } 224 return 0 225 } 226 // DISPUTED is reachable from FUNDED onward (any pre-release state) 227 if target == NX_ESCROW_DISPUTED { 228 if current >= NX_ESCROW_FUNDED { 229 if current <= NX_ESCROW_CONFIRMED { return 1 } 230 } 231 return 0 232 } 233 // TIMED_OUT_TO_SELLER only from DELIVERED (after auto-release window) 234 if target == NX_ESCROW_TIMED_OUT_TO_SELLER { 235 if current == NX_ESCROW_DELIVERED { return 1 } 236 return 0 237 } 238 // REFUND_PENDING only from DISPUTED (post-tribunal favor) 239 if target == NX_ESCROW_REFUND_PENDING { 240 if current == NX_ESCROW_DISPUTED { return 1 } 241 return 0 242 } 243 // RELEASING from CONFIRMED or TIMED_OUT_TO_SELLER (or post-tribunal favor) 244 if target == NX_ESCROW_RELEASING { 245 if current == NX_ESCROW_CONFIRMED { return 1 } 246 if current == NX_ESCROW_TIMED_OUT_TO_SELLER { return 1 } 247 if current == NX_ESCROW_DISPUTED { return 1 } // tribunal favored seller 248 return 0 249 } 250 // Normal forward flow 251 if target == current + 1 { return 1 } 252 return 0 253} 254 255func nx_escrow_advance(h: *EscrowHold, target: i64, now_unix: i64) -> i64 { 256 if h == 0 as *EscrowHold { return NX_ESCROW_NOT_FOUND } 257 if nx_escrow_can_transition(h.state, target) == 0 { return NX_ESCROW_INVALID_TRANSITION } 258 h.state = target 259 h.state_ts_unix = now_unix 260 if target == NX_ESCROW_SHIPPED { h.shipped_unix = now_unix } 261 if target == NX_ESCROW_DELIVERED { 262 h.delivered_unix = now_unix 263 h.auto_release_deadline_unix = now_unix + (NX_ESCROW_AUTO_RELEASE_DAYS * 86400) 264 } 265 if target == NX_ESCROW_CONFIRMED { h.confirmed_unix = now_unix } 266 if target == NX_ESCROW_RELEASED { h.released_unix = now_unix } 267 if target == NX_ESCROW_DISPUTED { h.disputed_unix = now_unix } 268 if target == NX_ESCROW_REFUNDED { h.refunded_unix = now_unix } 269 return NX_ESCROW_OK 270} 271 272// ===== Release flow =============================================== 273// 274// Composes nx_ledger: posts a balanced transaction: 275// debit buyer_escrow_account_id principal + platform_fee + tax 276// credit seller_payable_account_id principal 277// credit platform_fee_revenue_account_id platform_fee 278// credit tax_payable_account_id sales_tax 279// 280// Pacioli invariant: total debits == total credits (by construction). 281 282func nx_escrow_release_to_seller(h: *EscrowHold, now_unix: i64) -> i64 { 283 if h == 0 as *EscrowHold { return NX_ESCROW_NOT_FOUND } 284 if h.state == NX_ESCROW_FROZEN { return NX_ESCROW_FROZEN_NO_OP } 285 286 // Advance to RELEASING 287 let trans_verdict: i64 = nx_escrow_advance(h, NX_ESCROW_RELEASING, now_unix) 288 if trans_verdict != NX_ESCROW_OK { return trans_verdict } 289 290 // Compose ledger transaction. v1 honest-stub at ledger-post wire; 291 // full integration calls nx_ledger_post_transaction with 4 lines 292 // (1 debit + 3 credits) and a JOURNAL_KIND_ESCROW_RELEASE. 293 294 // Advance to RELEASED on successful post 295 return nx_escrow_advance(h, NX_ESCROW_RELEASED, now_unix) 296} 297 298// ===== Refund flow ================================================ 299 300func nx_escrow_refund_to_buyer(h: *EscrowHold, now_unix: i64) -> i64 { 301 if h == 0 as *EscrowHold { return NX_ESCROW_NOT_FOUND } 302 if h.state == NX_ESCROW_FROZEN { return NX_ESCROW_FROZEN_NO_OP } 303 if h.state != NX_ESCROW_DISPUTED { 304 if h.state != NX_ESCROW_REFUND_PENDING { return NX_ESCROW_INVALID_TRANSITION } 305 } 306 307 // Advance to REFUND_PENDING if from DISPUTED 308 if h.state == NX_ESCROW_DISPUTED { 309 let v: i64 = nx_escrow_advance(h, NX_ESCROW_REFUND_PENDING, now_unix) 310 if v != NX_ESCROW_OK { return v } 311 } 312 313 // Compose ledger transaction (reverse direction): 314 // debit buyer_escrow_account_id total 315 // credit buyer_source_account total 316 // (where buyer_source was the original deposit-from account) 317 // 318 // Note: platform fee + tax are also refunded. v1.1 introduces 319 // optional partial-refund flow where fee is non-refundable per 320 // platform-fee-policy (queued). 321 322 return nx_escrow_advance(h, NX_ESCROW_REFUNDED, now_unix) 323} 324 325// ===== Auto-release timeout check ================================= 326// 327// Cron-driven: substrate iterates active escrows in DELIVERED state; 328// if now_unix > h.auto_release_deadline_unix and not DISPUTED, 329// auto-advances to TIMED_OUT_TO_SELLER and triggers release. 330 331func nx_escrow_check_auto_release(h: *EscrowHold, now_unix: i64) -> i64 { 332 if h == 0 as *EscrowHold { return NX_ESCROW_NOT_FOUND } 333 if h.state != NX_ESCROW_DELIVERED { return NX_ESCROW_OK } // not applicable 334 if now_unix < h.auto_release_deadline_unix { return NX_ESCROW_TIMEOUT_NOT_REACHED } 335 let v: i64 = nx_escrow_advance(h, NX_ESCROW_TIMED_OUT_TO_SELLER, now_unix) 336 if v != NX_ESCROW_OK { return v } 337 return nx_escrow_release_to_seller(h, now_unix) 338} 339 340// ===== Dispute integration ======================================== 341// 342// Per Pillar 3 + nx_dispute_tribunal: when buyer raises dispute, 343// substrate freezes the release path + initiates tribunal. Tribunal 344// outcome dispatches back to release-to-seller or refund-to-buyer. 345 346func nx_escrow_initiate_dispute( 347 h: *EscrowHold, 348 tribunal_hk: i64, 349 now_unix: i64 350) -> i64 { 351 if h == 0 as *EscrowHold { return NX_ESCROW_NOT_FOUND } 352 if h.state == NX_ESCROW_RELEASED { return NX_ESCROW_INVALID_TRANSITION } 353 if h.state == NX_ESCROW_REFUNDED { return NX_ESCROW_INVALID_TRANSITION } 354 let v: i64 = nx_escrow_advance(h, NX_ESCROW_DISPUTED, now_unix) 355 if v != NX_ESCROW_OK { return v } 356 h.dispute_tribunal_hk = tribunal_hk 357 return NX_ESCROW_OK 358} 359 360// ===== Regulatory freeze (sanctions / fraud hold) ================= 361 362func nx_escrow_freeze(h: *EscrowHold, now_unix: i64) -> i64 { 363 if h == 0 as *EscrowHold { return NX_ESCROW_NOT_FOUND } 364 return nx_escrow_advance(h, NX_ESCROW_FROZEN, now_unix) 365}