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#[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 #[command(name = "generate-cids")]
22 GenerateCids(GenerateCidsCommand),
23}
24
25#[derive(Debug, Parser)]
27pub struct GenerateCidsCommand {
28 #[arg(short, long, default_value = "policy-files")]
30 directory: PathBuf,
31
32 #[arg(long)]
34 entrypoint: String,
35
36 #[arg(long, env = "PINATA_JWT")]
38 pinata_jwt: Option<String>,
39
40 #[arg(long, env = "PINATA_GATEWAY")]
42 pinata_gateway: Option<String>,
43
44 #[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, }
71
72impl GenerateCidsCommand {
73 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 if !file.exists() {
82 eyre::bail!("Error: file not found: {:?}", file);
83 }
84
85 let file_data = std::fs::read(&file).with_context(|| format!("Failed to read file: {:?}", file))?;
87
88 let file_name_from_path = file.file_name().and_then(|n| n.to_str()).unwrap_or("file");
90
91 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 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 let response_json: serde_json::Value = response.json().await.context("Failed to parse Pinata response")?;
121
122 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 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 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 pub async fn execute(self: Box<Self>, _config: NewtonAvsConfig<NewtonCliConfig>) -> Result<()> {
148 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 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 for file_upload in files_to_upload {
217 tracing::info!("================================================");
218 tracing::info!("{}", file_upload.header);
219 tracing::info!("================================================");
220
221 if !file_upload.path.exists() {
223 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 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 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 let json_content =
284 serde_json::to_string_pretty(&policy_cids).context("Failed to serialize policy_cids to JSON")?;
285
286 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 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}