1use alloy::primitives::{Address, U256};
2use clap::{Parser, Subcommand};
3use eyre::Context;
4use newton_prover_core::config::NewtonAvsConfig;
5use serde_json::Value;
6
7use crate::types::{CreateTaskRequest, TaskIntent};
8use std::{
9 path::PathBuf,
10 sync::atomic::{AtomicU64, Ordering},
11};
12use tracing::info;
13
14use crate::config::NewtonCliConfig;
15
16#[derive(Debug, Parser)]
18#[command(name = "task")]
19pub struct TaskCommand {
20 #[command(subcommand)]
21 pub subcommand: TaskSubcommand,
22}
23
24#[derive(Debug, Subcommand)]
25pub enum TaskSubcommand {
26 #[command(name = "submit-evaluation-request")]
28 SubmitEvaluationRequest(SubmitEvaluationRequestCommand),
29}
30
31#[derive(Debug, Parser)]
33pub struct SubmitEvaluationRequestCommand {
34 #[arg(long)]
36 task_json: PathBuf,
37
38 #[arg(long, env = "PRIVATE_KEY")]
39 private_key: Option<String>,
40
41 #[arg(long, env = "API_KEY")]
43 api_key: Option<String>,
44}
45
46static NEXT_ID: AtomicU64 = AtomicU64::new(0);
48
49fn get_next_id() -> u64 {
50 NEXT_ID.fetch_add(1, Ordering::Relaxed) + 1
51}
52
53fn create_json_rpc_request_payload(method: &str, params: serde_json::Value) -> serde_json::Value {
54 serde_json::json!({
55 "jsonrpc": "2.0",
56 "id": get_next_id(),
57 "method": method,
58 "params": params,
59 })
60}
61
62fn hex_to_u256(hex_str: &str) -> eyre::Result<U256> {
64 let hex_str = hex_str.strip_prefix("0x").unwrap_or(hex_str);
65 U256::from_str_radix(hex_str, 16).map_err(|e| eyre::eyre!("Failed to parse hex string '{}': {}", hex_str, e))
66}
67
68fn get_gateway_url(chain_id: u64, deployment_env: &str) -> eyre::Result<String> {
69 match (deployment_env, chain_id) {
70 ("stagef", 11155111 | 84532) => Ok("https://gateway.stagef.testnet.newton.xyz".to_string()),
72 ("stagef", 1) => Ok("https://gateway.stagef.newton.xyz".to_string()),
73 ("prod", 11155111 | 84532) => Ok("https://gateway.testnet.newton.xyz".to_string()),
74 ("prod", 1) => Ok("https://gateway.newton.xyz".to_string()),
75 _ => Err(eyre::eyre!(
76 "Unsupported combination: DEPLOYMENT_ENV={}, CHAIN_ID={}",
77 deployment_env,
78 chain_id
79 )),
80 }
81}
82
83async fn http_post(url: &str, body: &serde_json::Value, api_key: Option<&str>) -> eyre::Result<serde_json::Value> {
84 let client = reqwest::Client::new();
85
86 let mut request = client.post(url).header("Content-Type", "application/json");
87
88 if let Some(key) = api_key {
89 request = request.header("x-newton-secret", key);
90 }
91
92 let response = request.json(body).send().await?;
93
94 let status = response.status();
95
96 if !status.is_success() {
97 let error_text = response.text().await?;
98 return Err(eyre::eyre!("HTTP error {}: {}", status, error_text));
99 }
100
101 let response_json: serde_json::Value = response.json().await?;
102 Ok(response_json)
103}
104
105fn normalize_to_u256(value: &serde_json::Value) -> eyre::Result<U256> {
107 match value {
108 serde_json::Value::String(s) => hex_to_u256(s),
109 serde_json::Value::Number(n) => {
110 let num = n.as_u64().ok_or_else(|| eyre::eyre!("Number too large for u64"))?;
111 Ok(U256::from(num))
112 }
113 _ => Err(eyre::eyre!("Invalid value type: expected string or number")),
114 }
115}
116
117fn normalize_intent(intent: &serde_json::Value) -> eyre::Result<serde_json::Value> {
119 let mut normalized = intent.clone();
120
121 if let Some(value) = normalized.get("value") {
123 let normalized_value = normalize_to_u256(value)?;
124 normalized["value"] = serde_json::Value::String(format!("0x{:x}", normalized_value));
125 }
126
127 if let Some(chain_id) = normalized.get("chainId") {
129 let normalized_chain_id = normalize_to_u256(chain_id)?;
130 normalized["chainId"] = serde_json::Value::String(format!("0x{:x}", normalized_chain_id));
131 }
132
133 Ok(normalized)
134}
135
136fn get_address(value: &Value) -> eyre::Result<Address> {
138 let s = value
139 .as_str()
140 .ok_or_else(|| eyre::eyre!("Expected string for address"))?;
141 s.parse::<Address>().map_err(|e| eyre::eyre!("Invalid address: {}", e))
142}
143
144fn get_u256(value: &Value) -> eyre::Result<U256> {
145 if let Some(s) = value.as_str() {
146 hex_to_u256(s)
147 } else if let Some(n) = value.as_u64() {
148 Ok(U256::from(n))
149 } else {
150 Err(eyre::eyre!("Expected string or number for U256"))
151 }
152}
153
154fn json_intent_to_task_intent(intent: &serde_json::Value) -> eyre::Result<TaskIntent> {
156 let normalized_intent = normalize_intent(intent)?;
158
159 let from = get_address(
160 normalized_intent
161 .get("from")
162 .ok_or_else(|| eyre::eyre!("Missing from"))?,
163 )?;
164 let to = get_address(normalized_intent.get("to").ok_or_else(|| eyre::eyre!("Missing to"))?)?;
165 let value = get_u256(
166 normalized_intent
167 .get("value")
168 .ok_or_else(|| eyre::eyre!("Missing value"))?,
169 )?;
170 let chain_id = get_u256(
171 normalized_intent
172 .get("chainId")
173 .ok_or_else(|| eyre::eyre!("Missing chainId"))?,
174 )?;
175
176 let data_str = normalized_intent
178 .get("data")
179 .ok_or_else(|| eyre::eyre!("Missing data"))?
180 .as_str()
181 .ok_or_else(|| eyre::eyre!("data must be a string"))?;
182 let data = if data_str.starts_with("0x") {
183 data_str.to_string()
184 } else {
185 format!("0x{}", data_str)
186 };
187
188 let function_signature = normalized_intent
190 .get("functionSignature")
191 .and_then(|v| v.as_str())
192 .map(|s| {
193 if s.starts_with("0x") {
194 s.to_string()
195 } else {
196 format!("0x{}", s)
197 }
198 })
199 .unwrap_or_default();
200
201 Ok(TaskIntent {
202 from,
203 to,
204 value,
205 data,
206 chain_id,
207 function_signature,
208 })
209}
210
211impl TaskCommand {
212 pub async fn execute(self: Box<Self>, config: NewtonAvsConfig<NewtonCliConfig>) -> eyre::Result<()> {
214 match self.subcommand {
215 TaskSubcommand::SubmitEvaluationRequest(cmd) => {
216 let api_key = cmd.api_key;
220
221 info!("Reading task JSON from: {:?}", cmd.task_json);
222 let contents = std::fs::read_to_string(&cmd.task_json)
223 .with_context(|| format!("Failed to read task JSON file: {:?}", cmd.task_json))?;
224
225 let task: serde_json::Value = serde_json::from_str(&contents)
226 .with_context(|| format!("Failed to parse task JSON: {:?}", cmd.task_json))?;
227
228 let intent_json = task
229 .get("intent")
230 .ok_or_else(|| eyre::eyre!("Missing 'intent' field in task"))?;
231
232 let task_intent = json_intent_to_task_intent(intent_json)?;
234
235 let intent_sig = task
236 .get("intentSignature")
237 .and_then(|v| v.as_str())
238 .map(|s| s.to_string());
239
240 let policy_client = get_address(
242 task.get("policyClient")
243 .ok_or_else(|| eyre::eyre!("Missing policyClient"))?,
244 )?;
245
246 let quorum_number = task.get("quorumNumber").and_then(|v| v.as_str()).map(|s| s.to_string());
248 let quorum_threshold_percentage = task
249 .get("quorumThresholdPercentage")
250 .and_then(|v| v.as_u64())
251 .map(|v| v as u8);
252 let wasm_args = task.get("wasmArgs").and_then(|v| v.as_str()).map(|s| s.to_string());
253 let timeout = task.get("timeout").and_then(|v| v.as_u64());
254
255 let request = CreateTaskRequest {
257 policy_client,
258 intent: task_intent,
259 intent_signature: intent_sig,
260 quorum_number,
261 quorum_threshold_percentage,
262 wasm_args,
263 timeout,
264 use_two_phase: None,
265 encrypted_data_refs: None,
266 user_signature: None,
267 app_signature: None,
268 proof_cid: task.get("proofCid").and_then(|v| v.as_str()).map(|s| s.to_string()),
269 user_pubkey: None,
270 app_pubkey: None,
271 include_validate_calldata: None,
272 };
273
274 let request_json = serde_json::to_value(&request).with_context(|| "Failed to serialize request")?;
276
277 let payload =
278 create_json_rpc_request_payload("newt_createTask", serde_json::Value::Array(vec![request_json]));
279
280 let chain_id = config.chain_id;
281 let deployment_env = std::env::var("DEPLOYMENT_ENV").unwrap_or_else(|_| "prod".to_string());
282 let gateway_url = get_gateway_url(chain_id, &deployment_env)?;
283
284 info!("Submitting evaluation request to: {}", gateway_url);
285 let response = http_post(&gateway_url, &payload, api_key.as_deref()).await?;
286
287 info!("Response: {}", serde_json::to_string_pretty(&response)?);
288
289 Ok(())
290 }
291 }
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298 use std::str::FromStr;
299
300 #[test]
301 fn test_get_next_id() {
302 let id1 = get_next_id();
305 let id2 = get_next_id();
306 let id3 = get_next_id();
307 assert!(id2 > id1);
308 assert!(id3 > id2);
309 }
310
311 #[test]
312 fn test_create_json_rpc_request_payload() {
313 let method = "test_method";
314 let params = serde_json::json!({"key": "value"});
315 let payload = create_json_rpc_request_payload(method, params.clone());
316
317 assert_eq!(payload["jsonrpc"], "2.0");
318 assert_eq!(payload["method"], method);
319 assert_eq!(payload["params"], params);
320 assert!(payload["id"].is_number());
321 }
322
323 #[test]
324 fn test_hex_to_u256_with_prefix() {
325 let result = hex_to_u256("0x1a2b").unwrap();
326 assert_eq!(result, U256::from(0x1a2b));
327 }
328
329 #[test]
330 fn test_hex_to_u256_without_prefix() {
331 let result = hex_to_u256("1a2b").unwrap();
332 assert_eq!(result, U256::from(0x1a2b));
333 }
334
335 #[test]
336 fn test_hex_to_u256_large_value() {
337 let large_hex = "0xffffffffffffffffffffffffffffffff";
338 let result = hex_to_u256(large_hex).unwrap();
339 assert!(result > U256::from(u64::MAX));
340 }
341
342 #[test]
343 fn test_hex_to_u256_invalid_hex() {
344 let result = hex_to_u256("0xinvalid");
345 assert!(result.is_err());
346 assert!(result.unwrap_err().to_string().contains("Failed to parse hex string"));
347 }
348
349 #[test]
350 fn test_hex_to_u256_empty_string() {
351 let result = hex_to_u256("");
352 assert!(result.is_ok() || result.is_err());
354 }
355
356 #[test]
357 fn test_get_gateway_url_stagef_sepolia() {
358 let url = get_gateway_url(11155111, "stagef").unwrap();
359 assert_eq!(url, "https://gateway.stagef.testnet.newton.xyz");
360 }
361
362 #[test]
363 fn test_get_gateway_url_stagef_mainnet() {
364 let url = get_gateway_url(1, "stagef").unwrap();
365 assert_eq!(url, "https://gateway.stagef.newton.xyz");
366 }
367
368 #[test]
369 fn test_get_gateway_url_prod_sepolia() {
370 let url = get_gateway_url(11155111, "prod").unwrap();
371 assert_eq!(url, "https://gateway.testnet.newton.xyz");
372 }
373
374 #[test]
375 fn test_get_gateway_url_prod_mainnet() {
376 let url = get_gateway_url(1, "prod").unwrap();
377 assert_eq!(url, "https://gateway.newton.xyz");
378 }
379
380 #[test]
381 fn test_get_gateway_url_stagef_base_sepolia() {
382 let url = get_gateway_url(84532, "stagef").unwrap();
383 assert_eq!(url, "https://gateway.stagef.testnet.newton.xyz");
384 }
385
386 #[test]
387 fn test_get_gateway_url_prod_base_sepolia() {
388 let url = get_gateway_url(84532, "prod").unwrap();
389 assert_eq!(url, "https://gateway.testnet.newton.xyz");
390 }
391
392 #[test]
393 fn test_get_gateway_url_unsupported() {
394 let result = get_gateway_url(999, "stagef");
395 assert!(result.is_err());
396 assert!(result.unwrap_err().to_string().contains("Unsupported combination"));
397 }
398
399 #[test]
400 fn test_normalize_to_u256_from_hex_string() {
401 let value = serde_json::json!("0x64");
402 let result = normalize_to_u256(&value).unwrap();
403 assert_eq!(result, U256::from(100));
404 }
405
406 #[test]
407 fn test_normalize_to_u256_from_decimal_string() {
408 let value = serde_json::json!("100");
411 let result = normalize_to_u256(&value).unwrap();
412 assert_eq!(result, U256::from(256));
414 }
415
416 #[test]
417 fn test_normalize_to_u256_from_number() {
418 let value = serde_json::json!(42);
419 let result = normalize_to_u256(&value).unwrap();
420 assert_eq!(result, U256::from(42));
421 }
422
423 #[test]
424 fn test_normalize_to_u256_invalid_type() {
425 let value = serde_json::json!(true);
426 let result = normalize_to_u256(&value);
427 assert!(result.is_err());
428 assert!(result.unwrap_err().to_string().contains("Invalid value type"));
429 }
430
431 #[test]
432 fn test_normalize_intent_with_value_and_chainid() {
433 let intent = serde_json::json!({
434 "value": "0x64",
435 "chainId": 11155111,
436 "from": "0x0000000000000000000000000000000000000001",
437 "to": "0x0000000000000000000000000000000000000002"
438 });
439 let result = normalize_intent(&intent).unwrap();
440 assert_eq!(result["value"], "0x64");
441 assert_eq!(result["chainId"], "0xaa36a7"); }
443
444 #[test]
445 fn test_normalize_intent_with_number_value() {
446 let intent = serde_json::json!({
447 "value": 100,
448 "chainId": "0x1",
449 "from": "0x0000000000000000000000000000000000000001",
450 "to": "0x0000000000000000000000000000000000000002"
451 });
452 let result = normalize_intent(&intent).unwrap();
453 assert_eq!(result["value"], "0x64"); assert_eq!(result["chainId"], "0x1");
455 }
456
457 #[test]
458 fn test_normalize_intent_without_value_or_chainid() {
459 let intent = serde_json::json!({
460 "from": "0x0000000000000000000000000000000000000001",
461 "to": "0x0000000000000000000000000000000000000002"
462 });
463 let result = normalize_intent(&intent).unwrap();
464 assert_eq!(result["from"], "0x0000000000000000000000000000000000000001");
465 assert_eq!(result["to"], "0x0000000000000000000000000000000000000002");
466 }
467
468 #[test]
469 fn test_normalize_intent_invalid_value() {
470 let intent = serde_json::json!({
471 "value": "invalid",
472 "chainId": 1
473 });
474 let result = normalize_intent(&intent);
475 assert!(result.is_err());
476 }
477
478 #[test]
479 fn test_get_address_valid() {
480 let value = serde_json::json!("0x0000000000000000000000000000000000000001");
481 let result = get_address(&value).unwrap();
482 assert_eq!(
483 result,
484 Address::from_str("0x0000000000000000000000000000000000000001").unwrap()
485 );
486 }
487
488 #[test]
489 fn test_get_address_invalid_type() {
490 let value = serde_json::json!(123);
491 let result = get_address(&value);
492 assert!(result.is_err());
493 assert!(result.unwrap_err().to_string().contains("Expected string"));
494 }
495
496 #[test]
497 fn test_get_address_invalid_address() {
498 let value = serde_json::json!("not_an_address");
499 let result = get_address(&value);
500 assert!(result.is_err());
501 assert!(result.unwrap_err().to_string().contains("Invalid address"));
502 }
503
504 #[test]
505 fn test_get_u256_from_hex_string() {
506 let value = serde_json::json!("0x64");
507 let result = get_u256(&value).unwrap();
508 assert_eq!(result, U256::from(100));
509 }
510
511 #[test]
512 fn test_get_u256_from_number() {
513 let value = serde_json::json!(42);
514 let result = get_u256(&value).unwrap();
515 assert_eq!(result, U256::from(42));
516 }
517
518 #[test]
519 fn test_get_u256_invalid_type() {
520 let value = serde_json::json!(true);
521 let result = get_u256(&value);
522 assert!(result.is_err());
523 assert!(result.unwrap_err().to_string().contains("Expected string or number"));
524 }
525
526 #[test]
527 fn test_json_intent_to_task_intent_complete() {
528 let intent = serde_json::json!({
529 "from": "0x0000000000000000000000000000000000000001",
530 "to": "0x0000000000000000000000000000000000000002",
531 "value": "0x64",
532 "chainId": 11155111,
533 "data": "0x1234"
534 });
535 let result = json_intent_to_task_intent(&intent).unwrap();
536 assert_eq!(
537 result.from,
538 Address::from_str("0x0000000000000000000000000000000000000001").unwrap()
539 );
540 assert_eq!(
541 result.to,
542 Address::from_str("0x0000000000000000000000000000000000000002").unwrap()
543 );
544 assert_eq!(result.value, U256::from(100));
545 assert_eq!(result.chain_id, U256::from(11155111));
546 assert_eq!(result.data, "0x1234");
547 }
548
549 #[test]
550 fn test_json_intent_to_task_intent_with_function_signature() {
551 let intent = serde_json::json!({
552 "from": "0x0000000000000000000000000000000000000001",
553 "to": "0x0000000000000000000000000000000000000002",
554 "value": 100,
555 "chainId": "0x1",
556 "data": "1234",
557 "functionSignature": "0xabcd"
558 });
559 let result = json_intent_to_task_intent(&intent).unwrap();
560 assert_eq!(result.function_signature, "0xabcd");
561 assert_eq!(result.data, "0x1234"); }
563
564 #[test]
565 fn test_json_intent_to_task_intent_without_function_signature() {
566 let intent = serde_json::json!({
567 "from": "0x0000000000000000000000000000000000000001",
568 "to": "0x0000000000000000000000000000000000000002",
569 "value": 100,
570 "chainId": 1,
571 "data": "0x1234"
572 });
573 let result = json_intent_to_task_intent(&intent).unwrap();
574 assert_eq!(result.function_signature, "");
575 }
576
577 #[test]
578 fn test_json_intent_to_task_intent_missing_from() {
579 let intent = serde_json::json!({
580 "to": "0x0000000000000000000000000000000000000002",
581 "value": 100,
582 "chainId": 1,
583 "data": "0x1234"
584 });
585 let result = json_intent_to_task_intent(&intent);
586 assert!(result.is_err());
587 assert!(result.unwrap_err().to_string().contains("Missing from"));
588 }
589
590 #[test]
591 fn test_json_intent_to_task_intent_missing_to() {
592 let intent = serde_json::json!({
593 "from": "0x0000000000000000000000000000000000000001",
594 "value": 100,
595 "chainId": 1,
596 "data": "0x1234"
597 });
598 let result = json_intent_to_task_intent(&intent);
599 assert!(result.is_err());
600 assert!(result.unwrap_err().to_string().contains("Missing to"));
601 }
602
603 #[test]
604 fn test_json_intent_to_task_intent_missing_value() {
605 let intent = serde_json::json!({
606 "from": "0x0000000000000000000000000000000000000001",
607 "to": "0x0000000000000000000000000000000000000002",
608 "chainId": 1,
609 "data": "0x1234"
610 });
611 let result = json_intent_to_task_intent(&intent);
612 assert!(result.is_err());
613 assert!(result.unwrap_err().to_string().contains("Missing value"));
614 }
615
616 #[test]
617 fn test_json_intent_to_task_intent_missing_chainid() {
618 let intent = serde_json::json!({
619 "from": "0x0000000000000000000000000000000000000001",
620 "to": "0x0000000000000000000000000000000000000002",
621 "value": 100,
622 "data": "0x1234"
623 });
624 let result = json_intent_to_task_intent(&intent);
625 assert!(result.is_err());
626 assert!(result.unwrap_err().to_string().contains("Missing chainId"));
627 }
628
629 #[test]
630 fn test_json_intent_to_task_intent_missing_data() {
631 let intent = serde_json::json!({
632 "from": "0x0000000000000000000000000000000000000001",
633 "to": "0x0000000000000000000000000000000000000002",
634 "value": 100,
635 "chainId": 1
636 });
637 let result = json_intent_to_task_intent(&intent);
638 assert!(result.is_err());
639 assert!(result.unwrap_err().to_string().contains("Missing data"));
640 }
641
642 #[test]
643 fn test_json_intent_to_task_intent_data_not_string() {
644 let intent = serde_json::json!({
645 "from": "0x0000000000000000000000000000000000000001",
646 "to": "0x0000000000000000000000000000000000000002",
647 "value": 100,
648 "chainId": 1,
649 "data": 123
650 });
651 let result = json_intent_to_task_intent(&intent);
652 assert!(result.is_err());
653 assert!(result.unwrap_err().to_string().contains("data must be a string"));
654 }
655}