1use alloy::primitives::Address;
2use clap::{Parser, Subcommand};
3use eyre::{Context, Result};
4use newton_prover_chainio::policy::PolicyController;
5use newton_prover_core::{
6 common::parse_intent,
7 config::NewtonAvsConfig,
8 rego::{evaluate, KycIdentityData, Value as RegoValue},
9};
10use serde_json::Value;
11use std::path::PathBuf;
12use tracing::{self, info};
13
14use crate::{commands::utils, config::NewtonCliConfig};
15
16#[derive(Debug, Parser)]
18#[command(name = "policy")]
19pub struct PolicyCommand {
20 #[command(subcommand)]
21 pub subcommand: PolicySubcommand,
22}
23
24#[derive(Debug, Subcommand)]
25pub enum PolicySubcommand {
26 Deploy(DeployCommand),
28 Simulate(SimulateCommand),
30}
31
32fn validate_non_empty_path(s: &str) -> Result<PathBuf, String> {
33 if s.trim().is_empty() {
34 Err(String::from("Path cannot be empty"))
35 } else {
36 Ok(PathBuf::from(s))
37 }
38}
39
40fn normalize_intent(mut intent: serde_json::Value) -> eyre::Result<serde_json::Value> {
43 if let Some(value_field) = intent.get_mut("value") {
45 let normalized = normalize_number_field(value_field).with_context(|| "Failed to normalize value field")?;
46 *value_field = serde_json::Value::String(normalized);
47 }
48
49 if let Some(chain_id_field) = intent.get_mut("chainId") {
51 let normalized = normalize_number_field(chain_id_field).with_context(|| "Failed to normalize chainId field")?;
52 *chain_id_field = serde_json::Value::String(normalized);
53 }
54
55 Ok(intent)
56}
57
58fn normalize_number_field(value: &serde_json::Value) -> eyre::Result<String> {
61 match value {
62 serde_json::Value::String(s) => {
63 if s.starts_with("0x") || s.starts_with("0X") {
65 let hex_str = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")).unwrap();
67
68 if let Ok(num) = u64::from_str_radix(hex_str, 16) {
70 Ok(num.to_string())
71 } else {
72 let num = alloy::primitives::U256::from_str_radix(hex_str, 16)
74 .map_err(|e| eyre::eyre!("Invalid hex string '{}': {}", s, e))?;
75 Ok(num.to_string())
76 }
77 } else {
78 if s.parse::<alloy::primitives::U256>().is_ok() || s.parse::<u64>().is_ok() {
81 Ok(s.clone())
82 } else {
83 Err(eyre::eyre!("Invalid number string '{}'", s))
84 }
85 }
86 }
87 serde_json::Value::Number(n) => {
88 if let Some(u) = n.as_u64() {
90 Ok(u.to_string())
91 } else if let Some(i) = n.as_i64() {
92 if i < 0 {
93 return Err(eyre::eyre!("Negative numbers are not supported: {}", i));
94 }
95 Ok(i.to_string())
96 } else {
97 Ok(n.to_string())
100 }
101 }
102 _ => Err(eyre::eyre!("Expected string or number, got: {}", value)),
103 }
104}
105
106#[derive(Debug, Parser)]
108pub struct SimulateCommand {
109 #[arg(long)]
111 wasm_file: PathBuf,
112
113 #[arg(long)]
115 rego_file: PathBuf,
116
117 #[arg(long)]
119 intent_json: PathBuf,
120
121 #[arg(long, default_value = "allow")]
124 entrypoint: String,
125
126 #[arg(long)]
129 wasm_args: Option<PathBuf>,
130
131 #[arg(long)]
134 policy_params_data: Option<PathBuf>,
135
136 #[arg(long)]
141 identity_data: Option<PathBuf>,
142}
143
144#[derive(Debug, Parser)]
146pub struct DeployCommand {
147 #[arg(long, env = "PRIVATE_KEY")]
148 private_key: Option<String>,
149
150 #[arg(long, env = "RPC_URL")]
151 rpc_url: Option<String>,
152
153 #[arg(long)]
155 policy_data_address: Address,
156
157 #[arg(long, value_parser = validate_non_empty_path)]
158 policy_cids: PathBuf,
159}
160
161impl DeployCommand {
162 async fn deploy_policy(
167 private_key: &str,
168 rpc_url: &str,
169 policy_cid: &str,
170 schema_cid: &str,
171 entrypoint: &str,
172 policy_data_address: Address,
173 policy_metadata_cid: &str,
174 ) -> Result<Address> {
175 let controller = PolicyController::new(private_key.to_string(), rpc_url.to_string());
176
177 let policy_data = vec![policy_data_address];
179
180 tracing::info!(
181 "Deploying policy:\n entrypoint: {}\n policyCid: {}\n schemaCid: {}\n policyData: {:?}\n metadataCid: {} \n",
182 entrypoint,
183 policy_cid,
184 schema_cid,
185 policy_data,
186 policy_metadata_cid
187 );
188
189 let (_receipt, policy_address) = controller
190 .deploy_policy(
191 policy_cid.to_string(),
192 schema_cid.to_string(),
193 entrypoint.to_string(),
194 policy_data,
195 policy_metadata_cid.to_string(),
196 )
197 .await
198 .map_err(|e| eyre::eyre!("Failed to deploy policy: {}", e))?;
199
200 tracing::info!("Policy deployed successfully at address: {}", policy_address);
201
202 Ok(policy_address)
203 }
204
205 pub async fn execute(self: Box<Self>, _config: NewtonAvsConfig<NewtonCliConfig>) -> eyre::Result<()> {
207 let private_key = self
209 .private_key
210 .ok_or_else(|| eyre::eyre!("private_key is required (use --private-key or PRIVATE_KEY env var)"))?;
211
212 let rpc_url = self
213 .rpc_url
214 .ok_or_else(|| eyre::eyre!("rpc_url is required (use --rpc-url or RPC_URL env var)"))?;
215
216 let json_content = std::fs::read_to_string(&self.policy_cids)
218 .with_context(|| format!("Failed to read policy_cids.json: {:?}", self.policy_cids))?;
219 let json: Value = serde_json::from_str(&json_content)
220 .with_context(|| format!("Failed to parse policy_cids.json: {:?}", self.policy_cids))?;
221
222 let policy_cid = json.get("policyCid").and_then(|v| v.as_str()).unwrap_or("");
224 let schema_cid = json.get("schemaCid").and_then(|v| v.as_str()).unwrap_or("");
225 let entrypoint = json.get("entrypoint").and_then(|v| v.as_str()).unwrap_or("");
226 let policy_metadata_cid = json.get("policyMetadataCid").and_then(|v| v.as_str()).unwrap_or("");
227
228 Self::deploy_policy(
230 &private_key,
231 &rpc_url,
232 policy_cid,
233 schema_cid,
234 entrypoint,
235 self.policy_data_address,
236 policy_metadata_cid,
237 )
238 .await?;
239 Ok(())
240 }
241}
242
243impl SimulateCommand {
244 pub async fn execute(self: Box<Self>, config: NewtonAvsConfig<NewtonCliConfig>) -> eyre::Result<()> {
246 info!("Starting Rego simulation...");
247
248 info!("Reading intent JSON from: {:?}", self.intent_json);
250 let intent_json_str = std::fs::read_to_string(&self.intent_json)
251 .with_context(|| format!("Failed to read intent JSON file: {:?}", self.intent_json))?;
252 let mut intent_value: serde_json::Value = serde_json::from_str(&intent_json_str)
253 .with_context(|| format!("Failed to parse intent JSON: {:?}", self.intent_json))?;
254
255 info!("Normalizing intent fields...");
257 intent_value = normalize_intent(intent_value).with_context(|| "Failed to normalize intent fields")?;
258
259 let wasm_input = if let Some(wasm_args_path) = &self.wasm_args {
261 info!("Reading WASM args from: {:?}", wasm_args_path);
262 let wasm_args_str = std::fs::read_to_string(wasm_args_path)
263 .with_context(|| format!("Failed to read WASM args file: {:?}", wasm_args_path))?;
264 serde_json::from_str::<serde_json::Value>(&wasm_args_str)
266 .with_context(|| format!("Failed to parse WASM args as JSON: {:?}", wasm_args_path))?;
267 wasm_args_str
268 } else {
269 "{}".to_string()
270 };
271
272 info!("Executing WASM file: {:?}", self.wasm_file);
274 let wasm_output = utils::execute_wasm(self.wasm_file, wasm_input, config)
275 .await
276 .with_context(|| "Failed to execute WASM file")?;
277
278 let policy_data: serde_json::Value = serde_json::from_str(&wasm_output)
280 .with_context(|| format!("Failed to parse WASM output as JSON: {}", wasm_output))?;
281
282 info!("Parsing intent...");
284 let parsed_intent = parse_intent(intent_value).with_context(|| "Failed to parse intent")?;
285
286 let parsed_intent_str: String = parsed_intent.into();
288
289 info!("Reading Rego policy from: {:?}", self.rego_file);
291 let policy = std::fs::read_to_string(&self.rego_file)
292 .with_context(|| format!("Failed to read Rego policy file: {:?}", self.rego_file))?;
293
294 let policy_params: serde_json::Value = if let Some(policy_params_path) = &self.policy_params_data {
296 info!("Reading policy params data from: {:?}", policy_params_path);
297 let policy_params_str = std::fs::read_to_string(policy_params_path)
298 .with_context(|| format!("Failed to read policy params data file: {:?}", policy_params_path))?;
299 serde_json::from_str(&policy_params_str)
300 .with_context(|| format!("Failed to parse policy params data as JSON: {:?}", policy_params_path))?
301 } else {
302 serde_json::json!({})
303 };
304
305 let policy_params_and_data = serde_json::json!({
307 "params": policy_params,
308 "data": policy_data,
309 });
310 let policy_params_and_data_str = policy_params_and_data.to_string();
311
312 info!("\n=== Data object for Rego evaluation ===");
314 info!("{}", serde_json::to_string_pretty(&policy_params_and_data)?);
315 info!("==========================================\n");
316
317 let entrypoint = if self.entrypoint.starts_with("data.") {
319 self.entrypoint.clone()
320 } else {
321 format!("data.{}", self.entrypoint)
322 };
323
324 let identity_data = if let Some(identity_path) = &self.identity_data {
325 info!("Reading identity data from: {:?}", identity_path);
326 let identity_str = std::fs::read_to_string(identity_path)
327 .with_context(|| format!("Failed to read identity data file: {:?}", identity_path))?;
328 let v: serde_json::Value = serde_json::from_str(&identity_str)
329 .with_context(|| format!("Failed to parse identity data JSON: {:?}", identity_path))?;
330 KycIdentityData {
331 reference_date: v
332 .get("reference_date")
333 .and_then(|x| x.as_str())
334 .unwrap_or("")
335 .to_string(),
336 status: v.get("status").and_then(|x| x.as_str()).unwrap_or("").to_string(),
337 selected_country_code: v
338 .get("selected_country_code")
339 .and_then(|x| x.as_str())
340 .unwrap_or("")
341 .to_string(),
342 address_subdivision: v
343 .get("address_subdivision")
344 .and_then(|x| x.as_str())
345 .unwrap_or("")
346 .to_string(),
347 address_country_code: v
348 .get("address_country_code")
349 .and_then(|x| x.as_str())
350 .unwrap_or("")
351 .to_string(),
352 birthdate: v.get("birthdate").and_then(|x| x.as_str()).unwrap_or("").to_string(),
353 expiration_date: v
354 .get("expiration_date")
355 .and_then(|x| x.as_str())
356 .unwrap_or("")
357 .to_string(),
358 issue_date: v.get("issue_date").and_then(|x| x.as_str()).unwrap_or("").to_string(),
359 issuing_authority: v
360 .get("issuing_authority")
361 .and_then(|x| x.as_str())
362 .unwrap_or("")
363 .to_string(),
364 }
365 } else {
366 KycIdentityData {
367 status: "approved".to_string(),
368 address_subdivision: "CA".to_string(),
369 address_country_code: "US".to_string(),
370 ..Default::default()
371 }
372 };
373
374 info!("Evaluating policy with entrypoint: {}", entrypoint);
376 let result = evaluate(
377 policy,
378 &policy_params_and_data_str,
379 &parsed_intent_str,
380 Some(Box::new(identity_data)),
381 &entrypoint,
382 None,
383 )
384 .with_context(|| "Failed to evaluate Rego policy")?;
385
386 match result {
388 RegoValue::Bool(b) => {
389 info!("Evaluation result: {}", b);
390 if b {
391 info!(" Policy evaluation: ALLOWED");
392 } else {
393 info!(" Policy evaluation: DENIED");
394 }
395 }
396 _ => {
397 info!("Evaluation result: {:?}", result);
398 if result == RegoValue::Undefined {
399 info!(" Policy evaluation: UNDEFINED (denied)");
400 } else {
401 info!("? Policy evaluation: {:?}", result);
402 }
403 }
404 }
405
406 Ok(())
407 }
408}
409
410impl PolicyCommand {
411 pub async fn execute(self: Box<Self>, config: NewtonAvsConfig<NewtonCliConfig>) -> eyre::Result<()> {
413 match self.subcommand {
414 PolicySubcommand::Deploy(command) => {
415 Box::new(command).execute(config).await?;
416 }
417 PolicySubcommand::Simulate(command) => {
418 Box::new(command).execute(config).await?;
419 }
420 }
421 Ok(())
422 }
423}