1use crate::telemetry::get_telemetry_snapshot;
16use serde::{Deserialize, Serialize};
17
18#[derive(Debug, Serialize, Deserialize)]
20pub struct ComputeCostReceipt {
21 pub query_id: String,
22 pub superblock_cost: usize,
23 pub sieve_ops_cost: usize,
24 pub vm_cycles_cost: usize,
25 pub total_sats_owed: u64,
26}
27
28impl ComputeCostReceipt {
29 pub fn generate(query_id: &str) -> Self {
36 let (io, sieve, vm) = get_telemetry_snapshot();
37
38 let io_cost = io * 10;
39 let sieve_cost = sieve * 1;
40 let vm_cost = vm * 5;
41
42 let total_micro_sats = io_cost + sieve_cost + vm_cost;
43 let total_sats_owed = (total_micro_sats as f64 / 1_000_000.0).ceil() as u64;
44
45 Self {
46 query_id: query_id.to_string(),
47 superblock_cost: io,
48 sieve_ops_cost: sieve,
49 vm_cycles_cost: vm,
50 total_sats_owed,
51 }
52 }
53
54 pub fn to_json(&self) -> String {
56 serde_json::to_string(self).unwrap_or_else(|_| "{}".to_string())
57 }
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63 use crate::telemetry::{
64 reset_telemetry, SIEVE_OPS_COUNT, SUPERBLOCK_IO_COUNT, VM_CYCLES_COUNT,
65 };
66 use std::sync::atomic::Ordering;
67
68 #[test]
69 fn test_rpc_receipt_generation() {
70 reset_telemetry();
71
72 SUPERBLOCK_IO_COUNT.fetch_add(1500, Ordering::Relaxed);
74 SIEVE_OPS_COUNT.fetch_add(0, Ordering::Relaxed);
75 VM_CYCLES_COUNT.fetch_add(45000, Ordering::Relaxed);
76
77 let receipt = ComputeCostReceipt::generate("test-tx-123");
78 let json = receipt.to_json();
79
80 assert!(json.contains("test-tx-123"), "JSON missing query ID");
81 assert_eq!(receipt.superblock_cost, 1500);
82 assert_eq!(receipt.vm_cycles_cost, 45000);
83
84 assert_eq!(receipt.total_sats_owed, 1);
88 }
89}
90
91pub const TAX_RATE_PERCENT: u64 = 12;
97
98#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
103pub struct TaxRecipient {
104 pub label: String,
106 pub ilp_address: String,
108 pub share_percent: u64,
110 pub use_nym: bool,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct TaxRecipientSuite {
118 pub jurisdiction_did: String,
119 pub recipients: Vec<TaxRecipient>,
120}
121
122impl TaxRecipientSuite {
123 pub fn validate(&self) -> Result<(), String> {
125 let total: u64 = self.recipients.iter().map(|r| r.share_percent).sum();
126 if total != 100 {
127 Err(format!(
128 "TaxRecipientSuite shares sum to {total}, must be 100"
129 ))
130 } else {
131 Ok(())
132 }
133 }
134
135 pub fn default_cooperative() -> Self {
139 Self {
140 jurisdiction_did: "did:gov:cooperative:commons".to_string(),
141 recipients: vec![
142 TaxRecipient {
143 label: "Cooperative Infrastructure Fund".to_string(),
144 ilp_address: "$ilp.qualia.coop/infrastructure".to_string(),
145 share_percent: 40,
146 use_nym: false,
147 },
148 TaxRecipient {
149 label: "Digital Rights Legal Defence".to_string(),
150 ilp_address: "$ilp.qualia.coop/legal-defence".to_string(),
151 share_percent: 30,
152 use_nym: false,
153 },
154 TaxRecipient {
155 label: "Open Source Sustainability Pool".to_string(),
156 ilp_address: "$ilp.qualia.coop/oss-sustainability".to_string(),
157 share_percent: 20,
158 use_nym: false,
159 },
160 TaxRecipient {
161 label: "Disaster Recovery Reserve".to_string(),
162 ilp_address: "$ilp.qualia.coop/disaster-reserve".to_string(),
163 share_percent: 10,
164 use_nym: false,
165 },
166 ],
167 }
168 }
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
173pub struct MicropaymentInstruction {
174 pub recipient_label: String,
175 pub ilp_address: String,
176 pub amount_micro_cents: u64,
177 pub use_nym: bool,
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct TaxDispatchPlan {
184 pub gross_amount_micro_cents: u64,
186 pub tax_pool_micro_cents: u64,
188 pub principal_remainder_micro_cents: u64,
190 pub instructions: Vec<MicropaymentInstruction>,
192}
193
194pub fn route_tax_payment(
197 gross_amount_micro_cents: u64,
198 suite: &TaxRecipientSuite,
199) -> Result<TaxDispatchPlan, String> {
200 suite.validate()?;
201
202 let tax_pool = (gross_amount_micro_cents * TAX_RATE_PERCENT) / 100;
203 let principal_remainder = gross_amount_micro_cents - tax_pool;
204
205 let mut instructions = Vec::with_capacity(suite.recipients.len());
206 let mut allocated: u64 = 0;
207
208 for (i, recipient) in suite.recipients.iter().enumerate() {
209 let amount = if i == suite.recipients.len() - 1 {
210 tax_pool - allocated
212 } else {
213 (tax_pool * recipient.share_percent) / 100
214 };
215 allocated += amount;
216
217 instructions.push(MicropaymentInstruction {
218 recipient_label: recipient.label.clone(),
219 ilp_address: recipient.ilp_address.clone(),
220 amount_micro_cents: amount,
221 use_nym: recipient.use_nym,
222 });
223 }
224
225 Ok(TaxDispatchPlan {
226 gross_amount_micro_cents,
227 tax_pool_micro_cents: tax_pool,
228 principal_remainder_micro_cents: principal_remainder,
229 instructions,
230 })
231}
232
233#[derive(Debug, Serialize, Deserialize)]
238pub struct ProviderTermsRequest {
239 pub provider_did: String,
240 pub proposed_ilp_offset: u64, pub data_usage_intent: String, pub tax_jurisdiction_did: String,
243}
244
245#[derive(Debug, Serialize, Deserialize, PartialEq)]
246pub enum NegotiationStatus {
247 Accept,
248 Reject(String),
249}
250
251#[derive(Debug, Serialize, Deserialize)]
253pub struct NegotiationResponse {
254 pub status: NegotiationStatus,
255 pub tax_dispatch_plan: Option<TaxDispatchPlan>,
257 pub tax_pool_micro_cents: u64,
259 pub principal_remainder_micro_cents: u64,
261}
262
263pub fn negotiate_provider_terms(
267 request: ProviderTermsRequest,
268 base_connectivity_cost: u64,
269 tax_suite: Option<TaxRecipientSuite>,
270) -> NegotiationResponse {
271 if request.proposed_ilp_offset < base_connectivity_cost {
273 return NegotiationResponse {
274 status: NegotiationStatus::Reject(
275 "INSUFFICIENT_OFFSET: Proposed ILP does not cover intrinsic connectivity costs."
276 .to_string(),
277 ),
278 tax_dispatch_plan: None,
279 tax_pool_micro_cents: 0,
280 principal_remainder_micro_cents: 0,
281 };
282 }
283
284 if request.data_usage_intent == "STRIP_FIDUCIARY_METADATA" {
286 return NegotiationResponse {
287 status: NegotiationStatus::Reject(
288 "VIOLATION: Data usage intent violates Knowledge Axioms (Fiduciary Supremacy)."
289 .to_string(),
290 ),
291 tax_dispatch_plan: None,
292 tax_pool_micro_cents: 0,
293 principal_remainder_micro_cents: 0,
294 };
295 }
296
297 let suite = tax_suite.unwrap_or_else(TaxRecipientSuite::default_cooperative);
299 let plan = route_tax_payment(request.proposed_ilp_offset, &suite).unwrap_or_else(|_| {
300 TaxDispatchPlan {
301 gross_amount_micro_cents: request.proposed_ilp_offset,
302 tax_pool_micro_cents: 0,
303 principal_remainder_micro_cents: request.proposed_ilp_offset,
304 instructions: vec![],
305 }
306 });
307
308 NegotiationResponse {
309 status: NegotiationStatus::Accept,
310 tax_pool_micro_cents: plan.tax_pool_micro_cents,
311 principal_remainder_micro_cents: plan.principal_remainder_micro_cents,
312 tax_dispatch_plan: Some(plan),
313 }
314}
315
316#[cfg(test)]
317mod negotiation_tests {
318 use super::*;
319
320 fn default_req(offset: u64) -> ProviderTermsRequest {
321 ProviderTermsRequest {
322 provider_did: "did:git:corp123".to_string(),
323 proposed_ilp_offset: offset,
324 data_usage_intent: "standard_routing".to_string(),
325 tax_jurisdiction_did: "did:gov:cooperative:commons".to_string(),
326 }
327 }
328
329 #[test]
330 fn test_negotiation_insufficient_funds() {
331 let res = negotiate_provider_terms(default_req(4000), 5000, None);
332 assert!(matches!(res.status, NegotiationStatus::Reject(_)));
333 assert_eq!(res.tax_pool_micro_cents, 0);
334 }
335
336 #[test]
337 fn test_negotiation_fiduciary_violation() {
338 let mut req = default_req(6000);
339 req.data_usage_intent = "STRIP_FIDUCIARY_METADATA".to_string();
340 let res = negotiate_provider_terms(req, 5000, None);
341 assert!(matches!(res.status, NegotiationStatus::Reject(_)));
342 }
343
344 #[test]
345 fn test_12_percent_tax_split() {
346 let res = negotiate_provider_terms(default_req(10_000), 5000, None);
348 assert_eq!(res.status, NegotiationStatus::Accept);
349 assert_eq!(res.tax_pool_micro_cents, 1_200);
350 assert_eq!(res.principal_remainder_micro_cents, 8_800);
351
352 let plan = res.tax_dispatch_plan.unwrap();
353 assert_eq!(plan.instructions.len(), 4);
355 let disbursed: u64 = plan.instructions.iter().map(|i| i.amount_micro_cents).sum();
356 assert_eq!(disbursed, 1_200, "All tax µ-cents must be fully disbursed");
357 }
358
359 #[test]
360 fn test_custom_suite_two_recipients() {
361 let suite = TaxRecipientSuite {
362 jurisdiction_did: "did:gov:au:ato".to_string(),
363 recipients: vec![
364 TaxRecipient {
365 label: "ATO Federal".to_string(),
366 ilp_address: "$ilp.ato.gov.au/federal".to_string(),
367 share_percent: 70,
368 use_nym: false,
369 },
370 TaxRecipient {
371 label: "State Revenue NSW".to_string(),
372 ilp_address: "$ilp.revenue.nsw.gov.au/gst".to_string(),
373 share_percent: 30,
374 use_nym: false,
375 },
376 ],
377 };
378 assert!(suite.validate().is_ok());
379
380 let plan = route_tax_payment(100_000, &suite).unwrap();
383 assert_eq!(plan.tax_pool_micro_cents, 12_000);
384 assert_eq!(plan.principal_remainder_micro_cents, 88_000);
385 assert_eq!(plan.instructions[0].amount_micro_cents, 8_400);
386 assert_eq!(plan.instructions[1].amount_micro_cents, 3_600);
387 }
388
389 #[test]
390 fn test_suite_validation_rejects_wrong_sum() {
391 let bad_suite = TaxRecipientSuite {
392 jurisdiction_did: "did:gov:test".to_string(),
393 recipients: vec![
394 TaxRecipient {
395 label: "A".into(),
396 ilp_address: "$a".into(),
397 share_percent: 60,
398 use_nym: false,
399 },
400 TaxRecipient {
401 label: "B".into(),
402 ilp_address: "$b".into(),
403 share_percent: 30,
404 use_nym: false,
405 },
406 ],
408 };
409 assert!(bad_suite.validate().is_err());
410 }
411
412 #[test]
413 fn test_no_rounding_loss() {
414 let suite = TaxRecipientSuite {
416 jurisdiction_did: "did:gov:test:rounding".to_string(),
417 recipients: (0..7)
418 .map(|i| TaxRecipient {
419 label: format!("Recipient {i}"),
420 ilp_address: format!("$ilp.test/{i}"),
421 share_percent: if i < 6 { 14 } else { 16 }, use_nym: false,
423 })
424 .collect(),
425 };
426 let plan = route_tax_payment(10_007, &suite).unwrap();
427 let disbursed: u64 = plan.instructions.iter().map(|i| i.amount_micro_cents).sum();
428 assert_eq!(disbursed, plan.tax_pool_micro_cents, "Zero rounding loss");
429 }
430}