qualia_core_db/services/swarm/
settlement.rs1use super::verify::VerificationVerdict;
18use super::SwarmError;
19use crate::modalities::value_flow::{commons_cost, eroi_viable};
20use crate::rpc::MicropaymentInstruction;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum EscrowState {
25 Offered,
27 Held,
29 ReleasedToProvider,
31 RefundedToRequester,
33}
34
35#[derive(Debug, Clone, PartialEq)]
38pub enum SettlementOutcome {
39 Pay(MicropaymentInstruction),
41 Refund {
43 amount_micro_units: u64,
44 reason: &'static str,
45 },
46}
47
48#[derive(Debug, Clone, PartialEq)]
50pub struct Escrow {
51 pub job_id: u64,
52 pub requester_did: u64,
53 pub provider_did: u64,
54 pub amount_micro_units: u64,
55 pub provider_ilp: String,
57 pub use_nym: bool,
58 pub state: EscrowState,
59}
60
61impl Escrow {
62 pub fn offer(
64 job_id: u64,
65 requester_did: u64,
66 provider_did: u64,
67 amount_micro_units: u64,
68 provider_ilp: impl Into<String>,
69 use_nym: bool,
70 ) -> Self {
71 Self {
72 job_id,
73 requester_did,
74 provider_did,
75 amount_micro_units,
76 provider_ilp: provider_ilp.into(),
77 use_nym,
78 state: EscrowState::Offered,
79 }
80 }
81
82 pub fn hold(&mut self) -> Result<(), SwarmError> {
84 if self.state != EscrowState::Offered {
85 return Err(SwarmError::InvalidEscrowState);
86 }
87 self.state = EscrowState::Held;
88 Ok(())
89 }
90
91 pub fn settle(
99 &mut self,
100 verdict: VerificationVerdict,
101 ) -> Result<SettlementOutcome, SwarmError> {
102 if self.state != EscrowState::Held {
103 return Err(SwarmError::InvalidEscrowState);
104 }
105 match verdict {
106 VerificationVerdict::Verified { .. } => {
107 self.state = EscrowState::ReleasedToProvider;
108 Ok(SettlementOutcome::Pay(MicropaymentInstruction {
109 recipient_label: format!("swarm-provider:{:016x}", self.provider_did),
110 ilp_address: self.provider_ilp.clone(),
111 amount_micro_cents: self.amount_micro_units,
112 use_nym: self.use_nym,
113 }))
114 }
115 VerificationVerdict::Rejected { reason } => {
116 self.state = EscrowState::RefundedToRequester;
117 Ok(SettlementOutcome::Refund {
118 amount_micro_units: self.amount_micro_units,
119 reason,
120 })
121 }
122 }
123 }
124}
125
126pub fn price_paid_job(production_cost: u64, roi_cap_percent: u64, max_roi_percent: u64) -> u64 {
130 commons_cost(production_cost, roi_cap_percent, max_roi_percent)
131}
132
133pub fn energy_viable(value_returned: u64, energy_invested: u64, min_ratio: f32) -> bool {
138 eroi_viable(value_returned, energy_invested, min_ratio)
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 fn held_escrow() -> Escrow {
146 let mut e = Escrow::offer(0xB0B_u64, 1, 2, 500, "$ilp.solar.node/pay", false);
147 e.hold().unwrap();
148 e
149 }
150
151 #[test]
152 fn verified_releases_a_payment_instruction() {
153 let mut e = held_escrow();
154 let out = e
155 .settle(VerificationVerdict::Verified { confidence: 0.999 })
156 .unwrap();
157 assert_eq!(e.state, EscrowState::ReleasedToProvider);
158 match out {
159 SettlementOutcome::Pay(instr) => {
160 assert_eq!(instr.amount_micro_cents, 500);
161 assert_eq!(instr.ilp_address, "$ilp.solar.node/pay");
162 }
163 _ => panic!("verified must produce a payment instruction"),
164 }
165 }
166
167 #[test]
168 fn rejected_refunds_and_pays_no_provider() {
169 let mut e = held_escrow();
170 let out = e
171 .settle(VerificationVerdict::Rejected {
172 reason: "A·B ≠ C (Freivalds)",
173 })
174 .unwrap();
175 assert_eq!(e.state, EscrowState::RefundedToRequester);
176 assert!(matches!(
177 out,
178 SettlementOutcome::Refund {
179 amount_micro_units: 500,
180 ..
181 }
182 ));
183 }
184
185 #[test]
186 fn cannot_settle_before_holding() {
187 let mut e = Escrow::offer(1, 1, 2, 500, "$ilp/x", false); assert_eq!(
189 e.settle(VerificationVerdict::Verified { confidence: 1.0 })
190 .unwrap_err(),
191 SwarmError::InvalidEscrowState
192 );
193 }
194
195 #[test]
196 fn cannot_double_settle() {
197 let mut e = held_escrow();
198 e.settle(VerificationVerdict::Verified { confidence: 1.0 })
199 .unwrap();
200 assert!(e
202 .settle(VerificationVerdict::Verified { confidence: 1.0 })
203 .is_err());
204 }
205
206 #[test]
207 fn price_is_roi_capped() {
208 assert_eq!(price_paid_job(1000, 50, 20), 1200);
210 }
211
212 #[test]
213 fn energy_gate_refuses_net_extractive_jobs() {
214 assert!(energy_viable(300, 100, 2.0)); assert!(!energy_viable(150, 100, 2.0)); }
217}