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: Option<String>,
26
27        /// Path to a file containing JSON-RPC params (alternative to --params)
28        #[arg(long)]
29        params_file: Option<String>,
30
31        /// PoI ID to bind this intent to
32        #[arg(long)]
33        poi_id: Option<String>,
34
35        /// Confidence score (0.0 – 1.0)
36        #[arg(long)]
37        confidence: Option<f64>,
38
39        /// JSON-RPC ID (default: 1)
40        #[arg(long, default_value = "1")]
41        id: u64,
42
43        /// Hostname to use for the RPC call (e.g., solana-devnet.backpac.xyz)
44        #[arg(long)]
45        x_backpac_hostname: Option<String>,
46    },
47
48    /// Check the current status of an execution intent.
49    Status {
50        /// Execution intent ID
51        intent_id: String,
52    },
53
54    /// Receiver-side verification of an intent's PoI and payment.
55    Verify {
56        /// Execution intent ID to verify
57        intent_id: String,
58
59        /// Receiver DID for ownership check
60        #[arg(long)]
61        receiver_did: Option<String>,
62
63        /// Minimum confidence threshold (default: 0.30)
64        #[arg(long)]
65        min_confidence: Option<f64>,
66    },
67
68    /// Poll until the intent reaches a terminal state.
69    Wait {
70        /// Execution intent ID
71        intent_id: String,
72
73        /// Poll interval in seconds (default: 2)
74        #[arg(long, default_value = "2")]
75        interval: u64,
76
77        /// Maximum wait time in seconds (default: 120)
78        #[arg(long, default_value = "120")]
79        timeout: u64,
80    },
81
82    /// View the historical state transitions for an intent.
83    History {
84        /// Execution intent ID
85        intent_id: String,
86
87        /// Maximum results to return (default: 50)
88        #[arg(long, default_value = "50")]
89        limit: u32,
90
91        /// Filter by specific status
92        #[arg(long)]
93        status: Option<String>,
94    },
95
96    /// List intents for the authenticated agent.
97    List {
98        /// Filter by status (e.g., CREATED, FINALIZED)
99        #[arg(long)]
100        status: Option<String>,
101
102        /// Filter by ISO timestamp (show intents since this time)
103        #[arg(long)]
104        since: Option<String>,
105
106        /// Maximum results to return (default: 20, max: 100)
107        #[arg(long, default_value = "20")]
108        limit: u32,
109    },
110}
111
112impl IntentArgs {
113    pub async fn execute(&self, cli: &Cli) -> Result<(), CairnError> {
114        let client = BackpacClient::new(cli.jwt.as_deref(), cli.api_url.as_deref());
115
116        match &self.command {
117            IntentCommands::Send {
118                method,
119                params,
120                params_file,
121                poi_id,
122                confidence,
123                id,
124                x_backpac_hostname,
125            } => {
126                // Resolve params from --params or --params-file
127                let params_str = match (params, params_file) {
128                    (Some(p), _) => p.clone(),
129                    (None, Some(f)) => {
130                        std::fs::read_to_string(f)
131                            .map_err(|e| CairnError::InvalidInput(format!("Cannot read params file: {}", e)))?
132                            .trim()
133                            .to_string()
134                    }
135                    (None, None) => {
136                        return Err(CairnError::InvalidInput(
137                            "Either --params or --params-file is required".to_string(),
138                        ));
139                    }
140                };
141
142                let params_val: serde_json::Value = serde_json::from_str(&params_str)
143                    .map_err(|e| CairnError::InvalidInput(format!("Invalid JSON params: {}", e)))?;
144
145                let body = json!({
146                    "jsonrpc": "2.0",
147                    "method": method,
148                    "params": params_val,
149                    "id": id,
150                });
151
152                let result = client
153                    .rpc_post(&body, poi_id.as_deref(), *confidence, x_backpac_hostname.as_deref())
154                    .await?;
155
156                output_json(&result, &cli.output);
157                Ok(())
158            }
159
160            IntentCommands::Status { intent_id } => {
161                let path = format!("/v1/intents/{}", intent_id);
162                let result = client.get(&path).await?;
163                output_json(&result, &cli.output);
164                Ok(())
165            }
166
167            IntentCommands::Verify {
168                intent_id,
169                receiver_did,
170                min_confidence,
171            } => {
172                let mut path = format!("/v1/intents/{}/verify", intent_id);
173                let mut query_parts: Vec<String> = Vec::new();
174
175                if let Some(did) = receiver_did {
176                    query_parts.push(format!("receiver_did={}", did));
177                }
178                if let Some(conf) = min_confidence {
179                    query_parts.push(format!("min_confidence={}", conf));
180                }
181                if !query_parts.is_empty() {
182                    path = format!("{}?{}", path, query_parts.join("&"));
183                }
184
185                let result = client.get(&path).await?;
186                output_json(&result, &cli.output);
187                Ok(())
188            }
189
190            IntentCommands::Wait {
191                intent_id,
192                interval,
193                timeout,
194            } => {
195                let start = std::time::Instant::now();
196                let max_duration = std::time::Duration::from_secs(*timeout);
197                let poll_interval = std::time::Duration::from_secs(*interval);
198                let terminal_states = ["FINALIZED", "ABORTED", "EXPIRED"];
199
200                loop {
201                    if start.elapsed() > max_duration {
202                        return Err(CairnError::Timeout);
203                    }
204
205                    let path = format!("/v1/intents/{}", intent_id);
206                    let result = client.get(&path).await?;
207
208                    let status = result
209                        .get("status")
210                        .and_then(|v| v.as_str())
211                        .unwrap_or("UNKNOWN");
212
213                    if terminal_states.contains(&status) {
214                        output_json(&result, &cli.output);
215                        return Ok(());
216                    }
217
218                    tokio::time::sleep(poll_interval).await;
219                }
220            }
221
222            IntentCommands::History { intent_id, limit, status } => {
223                let mut path = format!("/v1/intents/{}/history?limit={}", intent_id, limit);
224                if let Some(s) = status {
225                    path = format!("{}&status={}", path, s);
226                }
227                let result = client.get(&path).await?;
228                output_json(&result, &cli.output);
229                Ok(())
230            }
231
232            IntentCommands::List {
233                status,
234                since,
235                limit,
236            } => {
237                let mut query_parts: Vec<String> = Vec::new();
238
239                if let Some(s) = status {
240                    query_parts.push(format!("status={}", s));
241                }
242                if let Some(s) = since {
243                    query_parts.push(format!("since={}", s));
244                }
245                query_parts.push(format!("limit={}", limit));
246
247                let path = format!("/v1/intents?{}", query_parts.join("&"));
248                let result = client.get(&path).await?;
249                output_json(&result, &cli.output);
250                Ok(())
251            }
252        }
253    }
254}