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