1use serde::{Deserialize, Serialize};
2
3use crate::{ConxianError, ConxianResult};
4
5const SETTLEMENT_ENVELOPE_VERSION_V2_LITERAL: &str = "2.0.0";
6
7pub const INSTITUTIONAL_TIMELOCK_BURN_BLOCKS: u64 = 144;
10pub const INSTITUTIONAL_ZAR_THRESHOLD_MAJOR: u64 = 100_000_000;
11
12pub const SETTLEMENT_ENVELOPE_VERSION_CURRENT: &str = SETTLEMENT_ENVELOPE_VERSION_V2_LITERAL;
14
15#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
18#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
19pub enum SanctionsRisk {
20 Low,
22 Medium,
24 High,
26 Critical,
28}
29
30#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
31#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
32pub enum SettlementSource {
33 Iso20022Pacs008,
34 Iso20022Pacs009,
35 Cips,
38 Papss,
39 Brics,
40 BricsPayDcms,
42 Spfs,
45 MBridge,
47 Erp,
48 DlcBond,
49 MachineToMachine,
52}
53
54#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
56#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
57pub enum M2MSettlementRail {
58 Lightning,
60 Peaq,
62 BitcoinOnChain,
64 TaprootAssets,
66}
67
68#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
70#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
71pub enum MachineService {
72 Charging,
74 Data,
76 Compute,
78 Storage,
80 Delivery,
82 Other,
84}
85
86impl SettlementSource {
87 pub fn as_rail_name(&self) -> &'static str {
88 match self {
89 Self::Iso20022Pacs008 | Self::Iso20022Pacs009 => "ISO20022",
90 Self::Cips => "CIPS",
91 Self::Papss => "PAPSS",
92 Self::Brics => "BRICS",
93 Self::BricsPayDcms => "BRICS_PAY_DCMS",
94 Self::Spfs => "SPFS",
95 Self::MBridge => "MBRIDGE",
96 Self::Erp => "ERP",
97 Self::DlcBond => "DLC_BOND",
98 Self::MachineToMachine => "M2M",
99 }
100 }
101
102 pub fn sanctions_risk(&self) -> SanctionsRisk {
105 match self {
106 Self::Iso20022Pacs008 | Self::Iso20022Pacs009 | Self::Erp => SanctionsRisk::Low,
108 Self::Papss | Self::Cips => SanctionsRisk::Medium,
110 Self::MBridge => SanctionsRisk::Medium,
112 Self::Brics => SanctionsRisk::Medium,
114 Self::BricsPayDcms => SanctionsRisk::High,
116 Self::Spfs => SanctionsRisk::Critical,
118 Self::DlcBond => SanctionsRisk::Low,
120 Self::MachineToMachine => SanctionsRisk::Low,
122 }
123 }
124
125 pub fn requires_sanctions_screening(&self) -> bool {
128 matches!(
129 self.sanctions_risk(),
130 SanctionsRisk::High | SanctionsRisk::Critical
131 )
132 }
133}
134
135#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
136#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
137pub enum SettlementRailFamily {
138 Rtgs,
139 Instant,
140 Ach,
141 Netting,
142 Other,
143}
144
145#[derive(Debug, Serialize, Deserialize, Clone)]
146pub struct SettlementRail {
147 pub family: SettlementRailFamily,
148 pub name: String,
149 pub region: String,
150}
151
152#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default)]
153#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
154pub enum SettlementFinality {
155 Final,
156 Provisional,
157 #[default]
158 Unknown,
159}
160
161#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
162#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
163pub enum SettlementStatus {
164 Ingested,
165 Accepted,
166 Rejected,
167 Settled,
168 Returned,
169 Reversed,
170 Recalled,
171}
172
173impl SettlementStatus {
174 pub fn parse(value: &str) -> Option<Self> {
175 match value.trim().to_ascii_uppercase().as_str() {
176 "INGESTED" => Some(Self::Ingested),
177 "ACCEPTED" => Some(Self::Accepted),
178 "REJECTED" => Some(Self::Rejected),
179 "SETTLED" => Some(Self::Settled),
180 "RETURNED" => Some(Self::Returned),
181 "REVERSED" => Some(Self::Reversed),
182 "RECALLED" => Some(Self::Recalled),
183 _ => None,
184 }
185 }
186}
187
188#[derive(Debug, Serialize, Deserialize, Clone, Default)]
189pub struct SettlementIdentifiers {
190 #[serde(skip_serializing_if = "Option::is_none")]
191 pub message_id: Option<String>,
192 #[serde(skip_serializing_if = "Option::is_none")]
193 pub transaction_reference: Option<String>,
194 #[serde(skip_serializing_if = "Option::is_none")]
195 pub settlement_reference: Option<String>,
196 #[serde(skip_serializing_if = "Option::is_none")]
197 pub end_to_end_id: Option<String>,
198 pub settlement_amount: String,
199 pub settlement_currency: String,
200 pub settlement_date: String, }
202
203#[derive(Debug, Serialize, Deserialize, Clone, Default)]
205pub struct IndustrialIntent {
206 pub sector: String,
207 pub project_id: String,
208 pub x402_payment_required: bool,
209 pub invoice_id: Option<String>,
210 pub device_id: Option<String>,
211}
212
213#[derive(Debug, Serialize, Deserialize, Clone)]
214pub struct NormalizedSettlement {
215 pub source: SettlementSource,
216 pub transaction_id: String,
217 pub amount_minor: u64,
218 pub amount_scale: u32,
219 pub currency: String,
220 pub sender: String,
221 pub receiver: String,
222 pub timestamp: u64,
223 pub status: SettlementStatus,
224 #[serde(default, skip_serializing_if = "Option::is_none")]
225 pub rail: Option<SettlementRail>,
226 #[serde(default)]
227 pub finality: SettlementFinality,
228 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub settled_at: Option<u64>,
230 pub identifiers: SettlementIdentifiers,
231 pub raw_payload_hash: String,
232 #[serde(default)]
233 pub industrial_intent: IndustrialIntent,
234}
235
236impl NormalizedSettlement {
237 pub fn requires_institutional_timelock(&self) -> bool {
239 if !self.currency.eq_ignore_ascii_case("ZAR") {
240 return false;
241 }
242
243 match institutional_threshold_minor(self.amount_scale) {
244 Some(threshold_minor) => u128::from(self.amount_minor) >= threshold_minor,
245 None => true,
246 }
247 }
248}
249
250fn institutional_threshold_minor(scale: u32) -> Option<u128> {
251 const MAX_SCALE: u32 = 38;
252 if scale > MAX_SCALE {
253 return None;
254 }
255
256 let factor = 10u128.checked_pow(scale)?;
257 u128::from(INSTITUTIONAL_ZAR_THRESHOLD_MAJOR).checked_mul(factor)
258}
259
260#[derive(Debug, Serialize, Deserialize, Clone)]
261pub struct SettlementEnvelope {
262 pub version: String,
263 pub payload: NormalizedSettlement,
264}
265
266#[derive(Debug, Serialize, Deserialize, Clone)]
271pub struct M2MSettlementRequest {
272 pub source_machine: crate::MachineIdentity,
274 pub target_machine: crate::MachineIdentity,
276 pub service_type: MachineService,
278 pub settlement_rail: M2MSettlementRail,
280 pub amount_minor: u64,
282 pub amount_scale: u32,
284 pub currency: String,
286 #[serde(default)]
288 pub payment_request: Option<String>,
289 #[serde(default)]
291 pub service_metadata: Option<serde_json::Value>,
292 pub timestamp: u64,
294 #[serde(default)]
296 pub idempotency_key: Option<String>,
297}
298
299#[derive(Debug, Serialize, Deserialize, Clone)]
301pub struct M2MSettlementResponse {
302 pub settlement_id: String,
304 pub status: SettlementStatus,
306 pub settlement_rail: M2MSettlementRail,
308 pub amount_minor: u64,
310 #[serde(default)]
312 pub settlement_proof: Option<String>,
313 pub settled_at: u64,
315}
316
317#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
318#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
319pub enum SettlementProposalState {
320 Proposed,
321 Timelocked,
322 Ready,
323 Executed,
324 Rejected,
325}
326
327#[derive(Debug, Serialize, Deserialize, Clone)]
328pub struct ProductiveStreaming {
329 pub founder_royalty_bps: u16, pub ecosystem_reserve_bps: u16, pub productive_yield_bps: u16, pub is_active: bool,
333}
334
335impl Default for ProductiveStreaming {
336 fn default() -> Self {
337 Self {
338 founder_royalty_bps: 500,
339 ecosystem_reserve_bps: 500,
340 productive_yield_bps: 9000,
341 is_active: true,
342 }
343 }
344}
345
346#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default)]
348#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
349pub enum FinanceTranche {
350 #[default]
351 Senior,
352 Junior,
353}
354
355#[derive(Debug, Serialize, Deserialize, Clone, Default)]
357pub struct OpsLoanMetadata {
358 pub loan_id: String,
359 pub tranche: FinanceTranche,
360 pub interest_accrued: f64,
361 pub principal_remaining: f64,
362}
363
364#[derive(Debug, Serialize, Deserialize, Clone)]
365pub struct SettlementProposal {
366 pub proposal_id: String,
367 pub envelope: SettlementEnvelope,
368 pub tee_attestation: crate::AttestationRequest,
369 pub stacks_burn_block_height: u64,
370 pub timelock_release_burn_block_height: u64,
371 pub created_at: u64,
372 pub state: SettlementProposalState,
373 pub streaming: ProductiveStreaming,
374 pub ops_loan: Option<OpsLoanMetadata>,
375}
376
377impl SettlementProposal {
378 pub fn new(
379 proposal_id: String,
380 envelope: SettlementEnvelope,
381 tee_attestation: crate::AttestationRequest,
382 stacks_burn_block_height: u64,
383 created_at: u64,
384 ) -> ConxianResult<Self> {
385 let requires_timelock = envelope.payload.requires_institutional_timelock();
386
387 let timelock_release_burn_block_height = if requires_timelock {
388 stacks_burn_block_height
389 .checked_add(INSTITUTIONAL_TIMELOCK_BURN_BLOCKS)
390 .ok_or_else(|| {
391 ConxianError::Internal(format!(
392 "Burn-block timelock release height overflow (proposal_id={proposal_id}, transaction_id={}, raw_payload_hash={}, base={stacks_burn_block_height}, delta={INSTITUTIONAL_TIMELOCK_BURN_BLOCKS})",
393 envelope.payload.transaction_id,
394 envelope.payload.raw_payload_hash
395 ))
396 })?
397 } else {
398 stacks_burn_block_height
399 };
400
401 let state = if requires_timelock {
402 SettlementProposalState::Timelocked
403 } else {
404 SettlementProposalState::Proposed
405 };
406
407 Ok(Self {
408 proposal_id,
409 envelope,
410 tee_attestation,
411 stacks_burn_block_height,
412 timelock_release_burn_block_height,
413 created_at,
414 state,
415 streaming: ProductiveStreaming::default(),
416 ops_loan: None,
417 })
418 }
419}