Skip to main content

conxian_core/
settlement.rs

1use serde::{Deserialize, Serialize};
2
3use crate::{ConxianError, ConxianResult};
4
5const SETTLEMENT_ENVELOPE_VERSION_V2_LITERAL: &str = "2.0.0";
6
7/// Institutional settlements above the regulatory threshold must be held in a burn-block timelock
8/// window before any execution is permitted.
9pub const INSTITUTIONAL_TIMELOCK_BURN_BLOCKS: u64 = 144;
10pub const INSTITUTIONAL_ZAR_THRESHOLD_MAJOR: u64 = 100_000_000;
11
12/// Current settlement envelope protocol version.
13pub const SETTLEMENT_ENVELOPE_VERSION_CURRENT: &str = SETTLEMENT_ENVELOPE_VERSION_V2_LITERAL;
14
15/// Sanctions-risk classification for settlement sources.
16/// Used by the ZKC verifier for jurisdictional screening and compliance reporting.
17#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
18#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
19pub enum SanctionsRisk {
20    /// No sanctions exposure — standard G7 rails (ISO 20022 via SWIFT/CHIPS)
21    Low,
22    /// Moderate exposure — CBDC bridges, regional payment systems
23    Medium,
24    /// Elevated exposure — alternative rails in sanctioned jurisdictions
25    High,
26    /// Critical exposure — sanctions-evasion specific infrastructure
27    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 (Cross-Border Interbank Payment System) — China-led,
36    /// $24.47T in 2024, 1,690 participants. Uses ISO 20022 with CIPS extensions.
37    Cips,
38    Papss,
39    Brics,
40    /// BRICS Pay DCMS — decentralized cross-border messaging (pilot phase)
41    BricsPayDcms,
42    /// SPFS (System for Transfer of Financial Messages) — Russia's SWIFT alternative,
43    /// 550 participants, under active US/EU sanctions.
44    Spfs,
45    /// mBridge — multi-CBDC bridge, EVM-compatible, 5 core + ~30 observing central banks
46    MBridge,
47    Erp,
48    DlcBond,
49    /// G-C3: Machine-to-Machine settlement — autonomous value transfer between DePIN
50    /// devices, AI agents, or IoT infrastructure. Non-custodial by design.
51    MachineToMachine,
52}
53
54/// G-C3: Which settlement rail carries an M2M payment.
55#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
56#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
57pub enum M2MSettlementRail {
58    /// Bitcoin Lightning Network — instant, sub-cent, non-custodial
59    Lightning,
60    /// peaq L1 chain — native DePIN settlement
61    Peaq,
62    /// Direct on-chain Bitcoin (for high-value machine settlements)
63    BitcoinOnChain,
64    /// Taproot Assets (USDT on Lightning)
65    TaprootAssets,
66}
67
68/// G-C3: Classification of machine-to-machine service types.
69#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
70#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
71pub enum MachineService {
72    /// EV charging (pay-per-kWh)
73    Charging,
74    /// Sensor / telemetry data sale
75    Data,
76    /// Compute resource leasing (CPU/GPU)
77    Compute,
78    /// Storage provisioning
79    Storage,
80    /// Autonomous delivery / logistics
81    Delivery,
82    /// Machine-to-machine service not otherwise classified
83    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    /// Returns the sanctions-risk classification for this settlement source.
103    /// Used by the ZKC verifier for compliance screening and audit logging.
104    pub fn sanctions_risk(&self) -> SanctionsRisk {
105        match self {
106            // G7-aligned rails — standard compliance profile
107            Self::Iso20022Pacs008 | Self::Iso20022Pacs009 | Self::Erp => SanctionsRisk::Low,
108            // Regional payment systems — moderate exposure
109            Self::Papss | Self::Cips => SanctionsRisk::Medium,
110            // CBDC bridge — post-BIS exit, being repositioned as BRICS Bridge
111            Self::MBridge => SanctionsRisk::Medium,
112            // Generic BRICS rail — mixed risk depending on corridor
113            Self::Brics => SanctionsRisk::Medium,
114            // Decentralized messaging — still in pilot, not yet sanctions-targeted
115            Self::BricsPayDcms => SanctionsRisk::High,
116            // Sanctions-evasion infrastructure — under active US/EU sanctions
117            Self::Spfs => SanctionsRisk::Critical,
118            // Bitcoin-native — sanctions-resistant by design (non-custodial)
119            Self::DlcBond => SanctionsRisk::Low,
120            // M2M — machine-native, non-custodial, sanctions-neutral
121            Self::MachineToMachine => SanctionsRisk::Low,
122        }
123    }
124
125    /// Returns true if this settlement source requires elevated compliance
126    /// screening due to sanctions exposure.
127    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, // YYYY-MM-DD
201}
202
203/// CON-451: Industrial Intent Metadata for x402 alignment
204#[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    /// Returns `true` when this settlement requires an institutional timelock window.
238    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// ── Machine-to-Machine Settlement (G-C3) ─────────────────────────────
267
268/// G-C3: M2M settlement request — an autonomous machine initiates a value transfer
269/// to another machine via the Conxian Gateway. Conxian routes; machines hold keys.
270#[derive(Debug, Serialize, Deserialize, Clone)]
271pub struct M2MSettlementRequest {
272    /// Identity of the source machine (sender)
273    pub source_machine: crate::MachineIdentity,
274    /// Identity of the target machine (receiver)
275    pub target_machine: crate::MachineIdentity,
276    /// The service being paid for
277    pub service_type: MachineService,
278    /// Which settlement rail to use
279    pub settlement_rail: M2MSettlementRail,
280    /// Amount in smallest unit (sats for BTC/Lightning, wei-equivalent for others)
281    pub amount_minor: u64,
282    /// Decimal scale for amount (8 for BTC, 18 for peaq)
283    pub amount_scale: u32,
284    /// Currency code: "BTC", "USDT", "PEAQ", etc.
285    pub currency: String,
286    /// Lightning invoice or payment request string (when rail == Lightning)
287    #[serde(default)]
288    pub payment_request: Option<String>,
289    /// Service metadata (kWh, GB-hours, compute-seconds, etc.)
290    #[serde(default)]
291    pub service_metadata: Option<serde_json::Value>,
292    /// Unix timestamp of the settlement request
293    pub timestamp: u64,
294    /// Idempotency key to prevent duplicate M2M settlements
295    #[serde(default)]
296    pub idempotency_key: Option<String>,
297}
298
299/// G-C3: M2M settlement response returned to the initiating machine.
300#[derive(Debug, Serialize, Deserialize, Clone)]
301pub struct M2MSettlementResponse {
302    /// Settlement identifier assigned by the Gateway
303    pub settlement_id: String,
304    /// Final status
305    pub status: SettlementStatus,
306    /// Rail used for settlement
307    pub settlement_rail: M2MSettlementRail,
308    /// Amount settled
309    pub amount_minor: u64,
310    /// Preimage (for Lightning payments) or txid
311    #[serde(default)]
312    pub settlement_proof: Option<String>,
313    /// When settlement finalized (Unix timestamp)
314    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,   // 5% (500 bps)
330    pub ecosystem_reserve_bps: u16, // 5% (500 bps)
331    pub productive_yield_bps: u16,  // 90% (9000 bps)
332    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/// CON-452: Structured Finance Tranches
347#[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/// CON-452: Operational Loan Metadata
356#[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}