Skip to main content

cairn_cli/cli/
intent.rs

1use clap::{Args, Subcommand};
2use serde_json::json;
3
4use crate::client::BackpacClient;
5use crate::errors::CairnError;
6
7use super::{output_json, Cli};
8
9#[derive(Args, Debug)]
10pub struct IntentArgs {
11    #[command(subcommand)]
12    pub command: IntentCommands,
13}
14
15#[derive(Subcommand, Debug)]
16pub enum IntentCommands {
17    /// Submit a JSON-RPC call as an execution intent with PoI binding.
18    Send {
19        /// JSON-RPC method (e.g., eth_sendRawTransaction)
20        #[arg(long)]
21        method: String,
22
23        /// JSON-RPC params as JSON array string
24        #[arg(long)]
25        params: String,
26
27        /// PoI ID to bind this intent to
28        #[arg(long)]
29        poi_id: Option<String>,
30
31        /// Confidence score (0.0 – 1.0)
32        #[arg(long)]
33        confidence: Option<f64>,
34
35        /// JSON-RPC ID (default: 1)
36        #[arg(long, default_value = "1")]
37        id: u64,
38    },
39
40    /// Check the current status of an execution intent.
41    Status {
42        /// Execution intent ID
43        intent_id: String,
44    },
45
46    /// Receiver-side verification of an intent's PoI and payment.
47    Verify {
48        /// Execution intent ID to verify
49        intent_id: String,
50
51        /// Receiver DID for ownership check
52        #[arg(long)]
53        receiver_did: Option<String>,
54
55        /// Minimum confidence threshold (default: 0.30)
56        #[arg(long)]
57        min_confidence: Option<f64>,
58    },
59
60    /// Poll until the intent reaches a terminal state.
61    Wait {
62        /// Execution intent ID
63        intent_id: String,
64
65        /// Poll interval in seconds (default: 2)
66        #[arg(long, default_value = "2")]
67        interval: u64,
68
69        /// Maximum wait time in seconds (default: 120)
70        #[arg(long, default_value = "120")]
71        timeout: u64,
72    },
73
74    /// List intents for the authenticated agent.
75    List {
76        /// Filter by status (e.g., CREATED, FINALIZED)
77        #[arg(long)]
78        status: Option<String>,
79
80        /// Filter by ISO timestamp (show intents since this time)
81        #[arg(long)]
82        since: Option<String>,
83
84        /// Maximum results to return (default: 20, max: 100)
85        #[arg(long, default_value = "20")]
86        limit: u32,
87    },
88}
89
90impl IntentArgs {
91    pub async fn execute(&self, cli: &Cli) -> Result<(), CairnError> {
92        let client = BackpacClient::new(cli.jwt.as_deref(), cli.api_url.as_deref());
93
94        match &self.command {
95            IntentCommands::Send {
96                method,
97                params,
98                poi_id,
99                confidence,
100                id,
101            } => {
102                let params_val: serde_json::Value = serde_json::from_str(params)
103                    .map_err(|e| CairnError::InvalidInput(format!("Invalid JSON params: {}", e)))?;
104
105                let body = json!({
106                    "jsonrpc": "2.0",
107                    "method": method,
108                    "params": params_val,
109                    "id": id,
110                });
111
112                let result = client
113                    .rpc_post(&body, poi_id.as_deref(), *confidence)
114                    .await?;
115
116                output_json(&result, &cli.output);
117                Ok(())
118            }
119
120            IntentCommands::Status { intent_id } => {
121                let path = format!("/v1/intents/{}", intent_id);
122                let result = client.get(&path).await?;
123                output_json(&result, &cli.output);
124                Ok(())
125            }
126
127            IntentCommands::Verify {
128                intent_id,
129                receiver_did,
130                min_confidence,
131            } => {
132                let mut path = format!("/v1/intents/{}/verify", intent_id);
133                let mut query_parts: Vec<String> = Vec::new();
134
135                if let Some(did) = receiver_did {
136                    query_parts.push(format!("receiver_did={}", did));
137                }
138                if let Some(conf) = min_confidence {
139                    query_parts.push(format!("min_confidence={}", conf));
140                }
141                if !query_parts.is_empty() {
142                    path = format!("{}?{}", path, query_parts.join("&"));
143                }
144
145                let result = client.get(&path).await?;
146                output_json(&result, &cli.output);
147                Ok(())
148            }
149
150            IntentCommands::Wait {
151                intent_id,
152                interval,
153                timeout,
154            } => {
155                let start = std::time::Instant::now();
156                let max_duration = std::time::Duration::from_secs(*timeout);
157                let poll_interval = std::time::Duration::from_secs(*interval);
158                let terminal_states = ["FINALIZED", "ABORTED", "EXPIRED"];
159
160                loop {
161                    if start.elapsed() > max_duration {
162                        return Err(CairnError::Timeout);
163                    }
164
165                    let path = format!("/v1/intents/{}", intent_id);
166                    let result = client.get(&path).await?;
167
168                    let status = result
169                        .get("status")
170                        .and_then(|v| v.as_str())
171                        .unwrap_or("UNKNOWN");
172
173                    if terminal_states.contains(&status) {
174                        output_json(&result, &cli.output);
175                        return Ok(());
176                    }
177
178                    tokio::time::sleep(poll_interval).await;
179                }
180            }
181
182            IntentCommands::List {
183                status,
184                since,
185                limit,
186            } => {
187                let mut query_parts: Vec<String> = Vec::new();
188
189                if let Some(s) = status {
190                    query_parts.push(format!("status={}", s));
191                }
192                if let Some(s) = since {
193                    query_parts.push(format!("since={}", s));
194                }
195                query_parts.push(format!("limit={}", limit));
196
197                let path = format!("/v1/intents?{}", query_parts.join("&"));
198                let result = client.get(&path).await?;
199                output_json(&result, &cli.output);
200                Ok(())
201            }
202        }
203    }
204}