newton_cli/commands/
policy_files.rs

1use clap::{Parser, Subcommand};
2use eyre::{Context, Result};
3use newton_prover_core::config::NewtonAvsConfig;
4use serde::{Deserialize, Serialize};
5use std::path::PathBuf;
6use tracing;
7
8use crate::config::NewtonCliConfig;
9
10/// Policy files commands
11#[derive(Debug, Parser)]
12#[command(name = "policy-files")]
13pub struct PolicyFilesCommand {
14    #[command(subcommand)]
15    pub subcommand: PolicyFilesSubcommand,
16}
17
18#[derive(Debug, Subcommand)]
19pub enum PolicyFilesSubcommand {
20    /// Generate CIDs for policy files
21    #[command(name = "generate-cids")]
22    GenerateCids(GenerateCidsCommand),
23}
24
25/// Generate CIDs command
26#[derive(Debug, Parser)]
27pub struct GenerateCidsCommand {
28    /// Directory containing policy files (defaults to policy-files)
29    #[arg(short, long, default_value = "policy-files")]
30    directory: PathBuf,
31
32    /// Entrypoint (e.g., "newton_trading_agent.allow")
33    #[arg(long)]
34    entrypoint: String,
35
36    /// Pinata JWT (or set PINATA_JWT env var)
37    #[arg(long, env = "PINATA_JWT")]
38    pinata_jwt: Option<String>,
39
40    /// Pinata Gateway (or set PINATA_GATEWAY env var)
41    #[arg(long, env = "PINATA_GATEWAY")]
42    pinata_gateway: Option<String>,
43
44    /// Output file path (defaults to policy-files/policy_cids.json)
45    #[arg(short, long, default_value = "policy-files/policy_cids.json")]
46    output: PathBuf,
47}
48
49#[derive(Debug, Serialize, Deserialize)]
50#[allow(non_snake_case)]
51struct PolicyCids {
52    wasmCid: String,
53    wasmArgs: String,
54    policyCid: String,
55    schemaCid: String,
56    attester: String,
57    entrypoint: String,
58    policyMetadataCid: String,
59    policyDataMetadataCid: String,
60}
61
62#[derive(Debug)]
63struct FileUpload {
64    name: String,
65    path: PathBuf,
66    mime_type: String,
67    default_name_prefix: String,
68    header: String,
69    json_key: String, // Key in the PolicyCids struct
70}
71
72impl GenerateCidsCommand {
73    /// Upload a file to Pinata IPFS
74    pub async fn upload_file_to_pinata(
75        file: PathBuf,
76        jwt: String,
77        file_name: String,
78        mime_type: &str,
79    ) -> Result<String> {
80        // Check if file exists
81        if !file.exists() {
82            eyre::bail!("Error: file not found: {:?}", file);
83        }
84
85        // Read file
86        let file_data = std::fs::read(&file).with_context(|| format!("Failed to read file: {:?}", file))?;
87
88        // Get file name from path
89        let file_name_from_path = file.file_name().and_then(|n| n.to_str()).unwrap_or("file");
90
91        // Create multipart form
92        let form = reqwest::multipart::Form::new()
93            .text("pinataOptions", r#"{"cidVersion":1}"#)
94            .text("pinataMetadata", format!(r#"{{"name":"{}"}}"#, file_name))
95            .part(
96                "file",
97                reqwest::multipart::Part::bytes(file_data)
98                    .file_name(file_name_from_path.to_string())
99                    .mime_str(mime_type)
100                    .unwrap(),
101            );
102
103        // Upload to Pinata
104        let client = reqwest::Client::new();
105        let response = client
106            .post("https://api.pinata.cloud/pinning/pinFileToIPFS")
107            .header("Authorization", format!("Bearer {}", jwt))
108            .multipart(form)
109            .send()
110            .await
111            .context("Failed to send request to Pinata")?;
112
113        if !response.status().is_success() {
114            let status = response.status();
115            let error_text = response.text().await.unwrap_or_default();
116            eyre::bail!("Pinata upload failed with status {}: {}", status, error_text);
117        }
118
119        // Parse response
120        let response_json: serde_json::Value = response.json().await.context("Failed to parse Pinata response")?;
121
122        // Extract IPFS hash
123        let ipfs_hash = response_json
124            .get("IpfsHash")
125            .and_then(|v| v.as_str())
126            .ok_or_else(|| eyre::eyre!("IPFS hash not found in response"))?;
127
128        Ok(ipfs_hash.to_string())
129    }
130
131    /// Display upload results
132    pub fn display_upload_results(ipfs_hash: &str, gateway: &str) {
133        tracing::info!("\n=== IPFS Upload Results ===");
134        tracing::info!("IPFS Hash: {}", ipfs_hash);
135        // Construct gateway links
136        let gateway_link = if gateway.ends_with('/') {
137            format!("{}{}", gateway, ipfs_hash)
138        } else {
139            format!("{}/{}", gateway, ipfs_hash)
140        };
141
142        tracing::info!(gateway_link = %gateway_link, "Direct IPFS Link");
143        tracing::info!("Public IPFS Link: https://ipfs.io/ipfs/{}", ipfs_hash);
144    }
145
146    /// Execute the generate-cids command
147    pub async fn execute(self: Box<Self>, _config: NewtonAvsConfig<NewtonCliConfig>) -> Result<()> {
148        // Get Pinata credentials
149        let jwt = self
150            .pinata_jwt
151            .ok_or_else(|| eyre::eyre!("Pinata JWT is required. Set PINATA_JWT env var or use --pinata-jwt"))?;
152
153        let gateway = self.pinata_gateway.ok_or_else(|| {
154            eyre::eyre!("Pinata Gateway is required. Set PINATA_GATEWAY env var or use --pinata-gateway")
155        })?;
156
157        // Define all files to upload in the correct order
158        let files_to_upload = vec![
159            FileUpload {
160                name: "policy.wasm".to_string(),
161                path: self.directory.join("policy.wasm"),
162                mime_type: "application/wasm".to_string(),
163                default_name_prefix: "newton-policy-wasm".to_string(),
164                header: "============ Upload policy.wasm ================".to_string(),
165                json_key: "wasmCid".to_string(),
166            },
167            FileUpload {
168                name: "wasm_args.json".to_string(),
169                path: self.directory.join("wasm_args.json"),
170                mime_type: "application/json".to_string(),
171                default_name_prefix: "newton-policy-wasm-args".to_string(),
172                header: "========== Upload wasm_args.json ===============".to_string(),
173                json_key: "wasmArgs".to_string(),
174            },
175            FileUpload {
176                name: "policy.rego".to_string(),
177                path: self.directory.join("policy.rego"),
178                mime_type: "text/plain".to_string(),
179                default_name_prefix: "newton-policy".to_string(),
180                header: "============== Upload policy.rego ==============".to_string(),
181                json_key: "policyCid".to_string(),
182            },
183            FileUpload {
184                name: "params_schema.json".to_string(),
185                path: self.directory.join("params_schema.json"),
186                mime_type: "application/json".to_string(),
187                default_name_prefix: "newton-policy-schema".to_string(),
188                header: "========== Upload params_schema.json ===========".to_string(),
189                json_key: "schemaCid".to_string(),
190            },
191            FileUpload {
192                name: "policy_metadata.json".to_string(),
193                path: self.directory.join("policy_metadata.json"),
194                mime_type: "application/json".to_string(),
195                default_name_prefix: "newton-policy-metadata".to_string(),
196                header: "========== Upload policy_metadata.json =========".to_string(),
197                json_key: "policyMetadataCid".to_string(),
198            },
199            FileUpload {
200                name: "policy_data_metadata.json".to_string(),
201                path: self.directory.join("policy_data_metadata.json"),
202                mime_type: "application/json".to_string(),
203                default_name_prefix: "newton-policy-data-metadata".to_string(),
204                header: "======== Upload policy_data_metadata.json ======".to_string(),
205                json_key: "policyDataMetadataCid".to_string(),
206            },
207        ];
208
209        tracing::info!("================================================");
210        tracing::info!("========== Uploading All Policy Files ==========");
211        tracing::info!("================================================");
212        tracing::info!("Directory: {:?}", self.directory);
213        tracing::info!("");
214        let mut ipfs_hashes: std::collections::HashMap<String, String> = std::collections::HashMap::new();
215        // Upload each file and collect IPFS hashes
216        for file_upload in files_to_upload {
217            tracing::info!("================================================");
218            tracing::info!("{}", file_upload.header);
219            tracing::info!("================================================");
220
221            // Check if file exists
222            if !file_upload.path.exists() {
223                // For wasm_args.json, it's optional, so use empty string
224                if file_upload.name == "wasm_args.json" {
225                    tracing::warn!("Warning: {} not found, using empty string...", file_upload.name);
226                    ipfs_hashes.insert(file_upload.json_key.clone(), String::new());
227                    continue;
228                }
229                eyre::bail!("Error: {} not found in {:?}", file_upload.name, self.directory);
230            }
231
232            // Generate file name
233            let file_name = format!(
234                "{}-{}",
235                file_upload.default_name_prefix,
236                chrono::Utc::now().format("%Y%m%d-%H%M%S")
237            );
238
239            tracing::info!(path = %file_upload.path.display(), "Uploading to Pinata IPFS");
240
241            match Self::upload_file_to_pinata(file_upload.path.clone(), jwt.clone(), file_name, &file_upload.mime_type)
242                .await
243            {
244                Ok(ipfs_hash) => {
245                    Self::display_upload_results(&ipfs_hash, &gateway);
246                    ipfs_hashes.insert(file_upload.json_key.clone(), ipfs_hash);
247                }
248                Err(e) => {
249                    eyre::bail!("Error uploading {}: {}", file_upload.name, e);
250                }
251            }
252            tracing::info!("");
253        }
254
255        // Create PolicyCids struct
256        let policy_cids = PolicyCids {
257            wasmCid: ipfs_hashes
258                .get("wasmCid")
259                .ok_or_else(|| eyre::eyre!("Missing wasmCid"))?
260                .clone(),
261            wasmArgs: ipfs_hashes.get("wasmArgs").unwrap_or(&String::new()).clone(),
262            policyCid: ipfs_hashes
263                .get("policyCid")
264                .ok_or_else(|| eyre::eyre!("Missing policyCid"))?
265                .clone(),
266            schemaCid: ipfs_hashes
267                .get("schemaCid")
268                .ok_or_else(|| eyre::eyre!("Missing schemaCid"))?
269                .clone(),
270            attester: "0x4883282094755C01cd0d15dFE74753c9E189d194".to_string(),
271            entrypoint: self.entrypoint.clone(),
272            policyMetadataCid: ipfs_hashes
273                .get("policyMetadataCid")
274                .ok_or_else(|| eyre::eyre!("Missing policyMetadataCid"))?
275                .clone(),
276            policyDataMetadataCid: ipfs_hashes
277                .get("policyDataMetadataCid")
278                .ok_or_else(|| eyre::eyre!("Missing policyDataMetadataCid"))?
279                .clone(),
280        };
281
282        // Write to JSON file
283        let json_content =
284            serde_json::to_string_pretty(&policy_cids).context("Failed to serialize policy_cids to JSON")?;
285
286        // Create parent directory if it doesn't exist
287        if let Some(parent) = self.output.parent() {
288            std::fs::create_dir_all(parent).with_context(|| format!("Failed to create directory: {:?}", parent))?;
289        }
290
291        std::fs::write(&self.output, json_content)
292            .with_context(|| format!("Failed to write to file: {:?}", self.output))?;
293
294        tracing::info!("================================================");
295        tracing::info!("========== Created policy_cids.json ============");
296        tracing::info!("================================================");
297        tracing::info!("Output file: {:?}", self.output);
298        tracing::info!("");
299        tracing::info!("{}", std::fs::read_to_string(&self.output)?);
300
301        tracing::info!("Successfully created policy_cids.json");
302
303        Ok(())
304    }
305}
306
307impl PolicyFilesCommand {
308    /// Execute the policy-files command
309    pub async fn execute(self: Box<Self>, config: NewtonAvsConfig<NewtonCliConfig>) -> eyre::Result<()> {
310        match self.subcommand {
311            PolicyFilesSubcommand::GenerateCids(command) => {
312                Box::new(command).execute(config).await?;
313            }
314        }
315        Ok(())
316    }
317}