Skip to main content

cairn_cli/cli/
rpc.rs

1use clap::Subcommand;
2use serde_json::json;
3
4use crate::client::BackpacClient;
5use crate::config::CairnConfig;
6use crate::errors::CairnError;
7use super::{output_json, Cli};
8
9#[derive(Subcommand, Debug)]
10pub enum RpcCommands {
11    /// Check network connectivity and agent authorization (Ping)
12    #[command(alias = "ping")]
13    Health,
14
15    /// Get the native token balance for an address
16    Balance {
17        /// Optional specific address to check (defaults to agent's own DID wallet)
18        #[arg(long, short)]
19        address: Option<String>,
20    },
21
22    /// Get the next transaction nonce for an address
23    Nonce {
24        /// Optional specific address to check
25        #[arg(long, short)]
26        address: Option<String>,
27    },
28
29    /// Request a gas/fee quote for the network
30    Quote,
31}
32
33impl RpcCommands {
34    pub async fn execute(&self, cli: &Cli) -> Result<(), CairnError> {
35        let config = CairnConfig::load();
36        let client = BackpacClient::new(cli.jwt.as_deref(), cli.api_url.as_deref());
37
38        // Resolve context
39        let chain = cli.chain.clone().unwrap_or(config.chain);
40        let network = cli.network.clone().unwrap_or(config.network);
41
42        if chain.is_empty() || network.is_empty() {
43            return Err(CairnError::InvalidInput(
44                "Chain and network must be specified via config, env, or flags".to_string(),
45            ));
46        }
47
48        let hostname = format!("{}-{}.backpac.xyz", chain, network);
49
50        match self {
51            RpcCommands::Health => {
52                // Send a simple chainId or health ping
53                let body = json!({
54                    "jsonrpc": "2.0",
55                    "id": 1,
56                    "method": "eth_chainId",
57                    "params": []
58                });
59
60                let resp = client.rpc_post(&body, None, None, Some(&hostname)).await?;
61                output_json(&resp, &cli.output);
62            }
63            RpcCommands::Balance { address } => {
64                let addr = match address {
65                    Some(a) => a.clone(),
66                    None => {
67                        let claims = client.claims().ok_or(CairnError::Auth("No valid JWT claims found to extract default address".into()))?;
68                        claims.get("wallet_address").and_then(|v| v.as_str()).unwrap_or("").to_string()
69                    }
70                };
71
72                if addr.is_empty() {
73                    return Err(CairnError::InvalidInput("Address could not be resolved".into()));
74                }
75
76                let body = json!({
77                    "jsonrpc": "2.0",
78                    "id": 1,
79                    "method": "eth_getBalance",
80                    "params": [addr, "latest"]
81                });
82
83                let resp = client.rpc_post(&body, None, None, Some(&hostname)).await?;
84                output_json(&resp, &cli.output);
85            }
86            RpcCommands::Nonce { address } => {
87                let addr = match address {
88                    Some(a) => a.clone(),
89                    None => {
90                        let claims = client.claims().ok_or(CairnError::Auth("No valid JWT claims found to extract default address".into()))?;
91                        claims.get("wallet_address").and_then(|v| v.as_str()).unwrap_or("").to_string()
92                    }
93                };
94
95                if addr.is_empty() {
96                    return Err(CairnError::InvalidInput("Address could not be resolved".into()));
97                }
98
99                let body = json!({
100                    "jsonrpc": "2.0",
101                    "id": 1,
102                    "method": "eth_getTransactionCount",
103                    "params": [addr, "latest"]
104                });
105
106                let resp = client.rpc_post(&body, None, None, Some(&hostname)).await?;
107                output_json(&resp, &cli.output);
108            }
109            RpcCommands::Quote => {
110                // Return gas price
111                let body = json!({
112                    "jsonrpc": "2.0",
113                    "id": 1,
114                    "method": "eth_gasPrice",
115                    "params": []
116                });
117
118                let resp = client.rpc_post(&body, None, None, Some(&hostname)).await?;
119                
120                // For Solana, eth_gasPrice might not map cleanly if the proxy doesn't support it, 
121                // but we stick to JSON-RPC norms for the E2E.
122                output_json(&resp, &cli.output);
123            }
124        }
125
126        Ok(())
127    }
128}