Skip to main content

cairn_cli/cli/
receive.rs

1use clap::Args;
2use serde_json::Value;
3
4use crate::client::BackpacClient;
5use crate::errors::CairnError;
6
7use super::{output_json, Cli};
8
9#[derive(Args, Debug)]
10pub struct ReceiveArgs {
11    /// ID of the Proof of Intent (PoI) to verify
12    #[arg(long, required = true)]
13    pub poi_id: String,
14
15    /// Sender address to validate against the PoI
16    #[arg(long)]
17    pub from: Option<String>,
18
19    /// Recipient DID or address to validate against the PoI context
20    #[arg(long)]
21    pub recipient: Option<String>,
22
23    /// Expected payment amount (as a string) to validate
24    #[arg(long)]
25    pub expect_value: Option<String>,
26
27    /// Require the intent to be finalized (PoI status SETTLED)
28    #[arg(long)]
29    pub require_finalized: bool,
30}
31
32impl ReceiveArgs {
33    pub async fn execute(&self, cli: &Cli) -> Result<(), CairnError> {
34        let client = BackpacClient::new(cli.jwt.as_deref(), cli.api_url.as_deref(), cli.worker_url.as_deref());
35
36        // 1. Fetch the PoI (using cross-agent permission model)
37        let mut query_parts = vec![];
38        if let Some(f) = &self.from {
39            query_parts.push(format!("from={}", f));
40        }
41        if let Some(r) = &self.recipient {
42            query_parts.push(format!("recipient={}", r));
43        }
44
45        let path = if query_parts.is_empty() {
46            format!("/v1/pois/{}", self.poi_id)
47        } else {
48            format!("/v1/pois/{}?{}", self.poi_id, query_parts.join("&"))
49        };
50
51        // If this fails (401/403/404), client returns CairnError
52        let poi: Value = client.get(&path).await?;
53
54        // 2. Validate expected value if provided
55        if let Some(expected_val) = &self.expect_value {
56            let actual_val = poi
57                .get("metadata")
58                .and_then(|m| m.get("amount").or_else(|| poi.get("amount"))) // Check both locations
59                .and_then(|a| {
60                    if a.is_number() {
61                        Some(a.to_string())
62                    } else {
63                        a.as_str().map(|s| s.to_string())
64                    }
65                });
66
67            if let Some(actual) = actual_val {
68                if actual != *expected_val {
69                    return Err(CairnError::ValueMismatch(format!(
70                        "Expected {}, got {}",
71                        expected_val, actual
72                    )));
73                }
74            } else {
75                return Err(CairnError::ValueMismatch(format!(
76                    "PoI does not contain an amount to verify against expected value {}",
77                    expected_val
78                )));
79            }
80        }
81
82        // 3. Status validation (Finalized/Settled)
83        let status = poi.get("status").and_then(|s| s.as_str()).unwrap_or("UNKNOWN");
84        if self.require_finalized && status != "SETTLED" && status != "FINALIZED" {
85            return Err(CairnError::NotFinalized(format!(
86                "Intent status is {}, but FINALIZED is required",
87                status
88            )));
89        }
90
91        // 4. Since PoI fetch succeeded and validated any value expectations,
92        // it acts as our receipt of valid intent structure.
93        // We output a unified verification status.
94        let verified_payload = serde_json::json!({
95            "poi_id": self.poi_id,
96            "status": status,
97            "verified": true,
98            "message": "Payment intent received and verified",
99            "poi": poi,
100        });
101
102        output_json(&verified_payload, &cli.output);
103        Ok(())
104    }
105}