Skip to main content

qualia_core_db/services/
rpc.rs

1//! JSON-RPC + Tax Router Subsystem
2//!
3//! Responsibilities:
4//!   1. Serialise engine telemetry into billing receipts (ILP / Lightning)
5//!   2. Evaluate incoming provider terms against the ILP Threshold Shift License
6//!   3. Split every accepted payment through the 12% Tax Router:
7//!         12% → divided across the TaxRecipientSuite (ILP micropayments)
8//!         88% → Principal's wallet
9//!
10//! The Tax Router is transport-agnostic — it produces a TaxDispatchPlan which
11//! the ILP layer (or future Lightning/Nym bridge) executes as discrete micropayments.
12//! Each recipient address is an ILP Payment Pointer ("$...") or a stablecoin
13//! wallet address ("did:..."). Nym mixnet routing is opt-in per recipient (`use_nym`).
14
15use crate::telemetry::get_telemetry_snapshot;
16use serde::{Deserialize, Serialize};
17
18/// A receipt detailing the exact Virtual Compute Cycles burned during a query.
19#[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    /// Generates a final billing receipt based on the current telemetry snapshot.
30    ///
31    /// Cost Weights (Mock values for Permissive Commons):
32    /// 1 SuperBlock IO = 10 micro-sats
33    /// 1 Sieve Op = 1 micro-sat
34    /// 1 VM Cycle = 5 micro-sats
35    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    /// Serializes the receipt to a JSON string for the external Lightning API.
55    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        // Burn virtual compute cycles
73        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        // 1500 * 10 = 15,000
85        // 45000 * 5 = 225,000
86        // Total Micro Sats = 240,000 => 1 Sat (Ceil)
87        assert_eq!(receipt.total_sats_owed, 1);
88    }
89}
90
91// ─── Tax Router ─────────────────────────────────────────────────────────────
92
93/// The fixed statutory tax rate applied to all incoming payments.
94/// 12% is split across the TaxRecipientSuite before the remainder
95/// reaches the Principal's wallet.
96pub const TAX_RATE_PERCENT: u64 = 12;
97
98/// A single named recipient in the tax disbursement suite.
99/// `ilp_address` is an ILP Payment Pointer ("$provider.example/account")
100/// or a stablecoin address ("did:wallet:...").
101/// `share_percent` must sum to 100 across all recipients in a suite.
102#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
103pub struct TaxRecipient {
104    /// Human-readable label, e.g. "Federal Revenue Service"
105    pub label: String,
106    /// ILP Payment Pointer or stablecoin address
107    pub ilp_address: String,
108    /// Percentage share of the total tax pool (0–100, all must sum to 100)
109    pub share_percent: u64,
110    /// Whether this payment should be routed via Nym mixnet for privacy
111    pub use_nym: bool,
112}
113
114/// A configured set of tax recipients for a jurisdiction.
115/// Example: Federal 60%, State 30%, Municipal 10%.
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct TaxRecipientSuite {
118    pub jurisdiction_did: String,
119    pub recipients: Vec<TaxRecipient>,
120}
121
122impl TaxRecipientSuite {
123    /// Validates that all share_percent values sum to exactly 100.
124    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    /// Returns a default cooperative suite using ILP payment pointers.
136    /// These point at the Cooperative Commons escrow accounts.
137    /// Replace with jurisdiction-specific addresses from the Tax Oracle.
138    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/// A single resolved micropayment instruction in a dispatch plan.
172#[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/// The complete dispatch plan produced by the Tax Router.
181/// The ILP layer executes each instruction as a discrete micropayment stream.
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct TaxDispatchPlan {
184    /// Total gross amount received (µ-cents)
185    pub gross_amount_micro_cents: u64,
186    /// The 12% tax pool (µ-cents)
187    pub tax_pool_micro_cents: u64,
188    /// The 88% principal remainder (µ-cents)
189    pub principal_remainder_micro_cents: u64,
190    /// Individual micropayment instructions for each recipient
191    pub instructions: Vec<MicropaymentInstruction>,
192}
193
194/// Routes a gross payment through the 12% Tax Router.
195/// Produces a TaxDispatchPlan ready for ILP execution.
196pub 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            // Last recipient gets the remainder to avoid rounding loss
211            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// ─── Provider Terms Negotiation ──────────────────────────────────────────────
234
235/// A request from an external corporate provider (e.g., ISP, Telemetry Aggregator)
236/// proposing terms to connect to the local Qualia-DB daemon.
237#[derive(Debug, Serialize, Deserialize)]
238pub struct ProviderTermsRequest {
239    pub provider_did: String,
240    pub proposed_ilp_offset: u64,  // µ-cents per GB
241    pub data_usage_intent: String, // N3Logic ruleset hash
242    pub tax_jurisdiction_did: String,
243}
244
245#[derive(Debug, Serialize, Deserialize, PartialEq)]
246pub enum NegotiationStatus {
247    Accept,
248    Reject(String),
249}
250
251/// The local agent's response to the provider's terms.
252#[derive(Debug, Serialize, Deserialize)]
253pub struct NegotiationResponse {
254    pub status: NegotiationStatus,
255    /// Full dispatch plan: 12% split across suite, 88% to Principal
256    pub tax_dispatch_plan: Option<TaxDispatchPlan>,
257    /// Convenience field: total µ-cents routed to tax suite
258    pub tax_pool_micro_cents: u64,
259    /// Convenience field: µ-cents retained by Principal
260    pub principal_remainder_micro_cents: u64,
261}
262
263/// Evaluates a corporate provider's connection request against the user's
264/// intrinsic ILP connectivity cost and Rights Ontology, then routes the
265/// accepted payment through the 12% Tax Router.
266pub fn negotiate_provider_terms(
267    request: ProviderTermsRequest,
268    base_connectivity_cost: u64,
269    tax_suite: Option<TaxRecipientSuite>,
270) -> NegotiationResponse {
271    // 1. ILP Threshold Shift Check
272    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    // 2. Fiduciary Supremacy Check
285    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    // 3. Route through 12% Tax Router
298    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        // 10000 µ-cents gross → 12% = 1200 tax pool → 8800 to Principal
347        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        // 4 recipients in default suite, shares sum to 100
354        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        // 100_000 µ-cents → 12% = 12_000 tax pool
381        // Federal: 70% of 12_000 = 8_400; State: 30% of 12_000 = 3_600
382        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                // Missing 10% — deliberately wrong
407            ],
408        };
409        assert!(bad_suite.validate().is_err());
410    }
411
412    #[test]
413    fn test_no_rounding_loss() {
414        // 7 recipients with awkward shares — last one absorbs rounding dust
415        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 }, // 6*14 + 16 = 100
422                    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}