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 intent_sig = task
252 .get("intentSignature")
253 .map(get_bytes)
254 .transpose()?
255 .unwrap_or_default();
256
257 let policy_client = get_address(
259 task.get("policyClient")
260 .ok_or_else(|| eyre::eyre!("Missing policyClient"))?,
261 )?;
262
263 let quorum_number = task.get("quorumNumber").map(get_bytes).transpose()?.unwrap_or_default();
265 let quorum_threshold_percentage = task
266 .get("quorumThresholdPercentage")
267 .and_then(|v| v.as_u64())
268 .map(|v| v as u32)
269 .unwrap_or(0);
270 let wasm_args = task.get("wasmArgs").map(get_bytes).transpose()?.unwrap_or_default();
271 let timeout = task.get("timeout").and_then(|v| v.as_u64()).unwrap_or(60);
272
273 let i_request = ICreateTaskAndWaitRequest {
275 policyClient: policy_client,
276 intent: i_task_intent,
277 intentSignature: intent_sig,
278 quorumNumber: quorum_number,
279 quorumThresholdPercentage: quorum_threshold_percentage,
280 wasmArgs: wasm_args,
281 timeout,
282 };
283
284 let signed_request = CreateTaskAndWaitRequest::signed_from(&i_request, &private_key);
286
287 let signed_request_json =
289 serde_json::to_value(&signed_request).with_context(|| "Failed to serialize signed request")?;
290
291 let payload = create_json_rpc_request_payload(
292 "newton_createTaskAndWait",
293 serde_json::Value::Array(vec![signed_request_json]),
294 );
295
296 let chain_id = config.chain_id;
297 let deployment_env = std::env::var("DEPLOYMENT_ENV").unwrap_or_else(|_| "prod".to_string());
298 let prover_avs_url = get_prover_avs_url(chain_id, &deployment_env)?;
299
300 info!("Submitting evaluation request to: {}", prover_avs_url);
301 let response = http_post(&prover_avs_url, &payload).await?;
302
303 info!("Response: {}", serde_json::to_string_pretty(&response)?);
304
305 Ok(())
306 }
307 }
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314 use std::str::FromStr;
315
316 #[test]
317 fn test_get_next_id() {
318 let id1 = get_next_id();
321 let id2 = get_next_id();
322 let id3 = get_next_id();
323 assert!(id2 > id1);
324 assert!(id3 > id2);
325 }
326
327 #[test]
328 fn test_create_json_rpc_request_payload() {
329 let method = "test_method";
330 let params = serde_json::json!({"key": "value"});
331 let payload = create_json_rpc_request_payload(method, params.clone());
332
333 assert_eq!(payload["jsonrpc"], "2.0");
334 assert_eq!(payload["method"], method);
335 assert_eq!(payload["params"], params);
336 assert!(payload["id"].is_number());
337 }
338
339 #[test]
340 fn test_hex_to_u256_with_prefix() {
341 let result = hex_to_u256("0x1a2b").unwrap();
342 assert_eq!(result, U256::from(0x1a2b));
343 }
344
345 #[test]
346 fn test_hex_to_u256_without_prefix() {
347 let result = hex_to_u256("1a2b").unwrap();
348 assert_eq!(result, U256::from(0x1a2b));
349 }
350
351 #[test]
352 fn test_hex_to_u256_large_value() {
353 let large_hex = "0xffffffffffffffffffffffffffffffff";
354 let result = hex_to_u256(large_hex).unwrap();
355 assert!(result > U256::from(u64::MAX));
356 }
357
358 #[test]
359 fn test_hex_to_u256_invalid_hex() {
360 let result = hex_to_u256("0xinvalid");
361 assert!(result.is_err());
362 assert!(result.unwrap_err().to_string().contains("Failed to parse hex string"));
363 }
364
365 #[test]
366 fn test_hex_to_u256_empty_string() {
367 let result = hex_to_u256("");
368 assert!(result.is_ok() || result.is_err());
370 }
371
372 #[test]
373 fn test_get_prover_avs_url_stagef_sepolia() {
374 let url = get_prover_avs_url(11155111, "stagef").unwrap();
375 assert_eq!(url, "https://prover-avs.stagef.sepolia.newt.foundation");
376 }
377
378 #[test]
379 fn test_get_prover_avs_url_stagef_mainnet() {
380 let url = get_prover_avs_url(1, "stagef").unwrap();
381 assert_eq!(url, "https://prover-avs.stagef.newt.foundation");
382 }
383
384 #[test]
385 fn test_get_prover_avs_url_prod_sepolia() {
386 let url = get_prover_avs_url(11155111, "prod").unwrap();
387 assert_eq!(url, "https://prover-avs.sepolia.newt.foundation");
388 }
389
390 #[test]
391 fn test_get_prover_avs_url_prod_mainnet() {
392 let url = get_prover_avs_url(1, "prod").unwrap();
393 assert_eq!(url, "https://prover-avs.newt.foundation");
394 }
395
396 #[test]
397 fn test_get_prover_avs_url_unsupported() {
398 let result = get_prover_avs_url(999, "stagef");
399 assert!(result.is_err());
400 assert!(result.unwrap_err().to_string().contains("Unsupported combination"));
401 }
402
403 #[test]
404 fn test_normalize_to_u256_from_hex_string() {
405 let value = serde_json::json!("0x64");
406 let result = normalize_to_u256(&value).unwrap();
407 assert_eq!(result, U256::from(100));
408 }
409
410 #[test]
411 fn test_normalize_to_u256_from_decimal_string() {
412 let value = serde_json::json!("100");
415 let result = normalize_to_u256(&value).unwrap();
416 assert_eq!(result, U256::from(256));
418 }
419
420 #[test]
421 fn test_normalize_to_u256_from_number() {
422 let value = serde_json::json!(42);
423 let result = normalize_to_u256(&value).unwrap();
424 assert_eq!(result, U256::from(42));
425 }
426
427 #[test]
428 fn test_normalize_to_u256_invalid_type() {
429 let value = serde_json::json!(true);
430 let result = normalize_to_u256(&value);
431 assert!(result.is_err());
432 assert!(result.unwrap_err().to_string().contains("Invalid value type"));
433 }
434
435 #[test]
436 fn test_normalize_intent_with_value_and_chainid() {
437 let intent = serde_json::json!({
438 "value": "0x64",
439 "chainId": 11155111,
440 "from": "0x0000000000000000000000000000000000000001",
441 "to": "0x0000000000000000000000000000000000000002"
442 });
443 let result = normalize_intent(&intent).unwrap();
444 assert_eq!(result["value"], "0x64");
445 assert_eq!(result["chainId"], "0xaa36a7"); }
447
448 #[test]
449 fn test_normalize_intent_with_number_value() {
450 let intent = serde_json::json!({
451 "value": 100,
452 "chainId": "0x1",
453 "from": "0x0000000000000000000000000000000000000001",
454 "to": "0x0000000000000000000000000000000000000002"
455 });
456 let result = normalize_intent(&intent).unwrap();
457 assert_eq!(result["value"], "0x64"); assert_eq!(result["chainId"], "0x1");
459 }
460
461 #[test]
462 fn test_normalize_intent_without_value_or_chainid() {
463 let intent = serde_json::json!({
464 "from": "0x0000000000000000000000000000000000000001",
465 "to": "0x0000000000000000000000000000000000000002"
466 });
467 let result = normalize_intent(&intent).unwrap();
468 assert_eq!(result["from"], "0x0000000000000000000000000000000000000001");
469 assert_eq!(result["to"], "0x0000000000000000000000000000000000000002");
470 }
471
472 #[test]
473 fn test_normalize_intent_invalid_value() {
474 let intent = serde_json::json!({
475 "value": "invalid",
476 "chainId": 1
477 });
478 let result = normalize_intent(&intent);
479 assert!(result.is_err());
480 }
481
482 #[test]
483 fn test_hex_to_bytes_with_prefix() {
484 let result = hex_to_bytes("0x1234").unwrap();
485 assert_eq!(result.len(), 2);
486 assert_eq!(result[0], 0x12);
487 assert_eq!(result[1], 0x34);
488 }
489
490 #[test]
491 fn test_hex_to_bytes_without_prefix() {
492 let result = hex_to_bytes("1234").unwrap();
493 assert_eq!(result.len(), 2);
494 assert_eq!(result[0], 0x12);
495 assert_eq!(result[1], 0x34);
496 }
497
498 #[test]
499 fn test_hex_to_bytes_empty() {
500 let result = hex_to_bytes("0x").unwrap();
501 assert_eq!(result.len(), 0);
502 }
503
504 #[test]
505 fn test_hex_to_bytes_invalid_hex() {
506 let result = hex_to_bytes("0xinvalid");
507 assert!(result.is_err());
508 assert!(result.unwrap_err().to_string().contains("Failed to decode hex"));
509 }
510
511 #[test]
512 fn test_get_address_valid() {
513 let value = serde_json::json!("0x0000000000000000000000000000000000000001");
514 let result = get_address(&value).unwrap();
515 assert_eq!(
516 result,
517 Address::from_str("0x0000000000000000000000000000000000000001").unwrap()
518 );
519 }
520
521 #[test]
522 fn test_get_address_invalid_type() {
523 let value = serde_json::json!(123);
524 let result = get_address(&value);
525 assert!(result.is_err());
526 assert!(result.unwrap_err().to_string().contains("Expected string"));
527 }
528
529 #[test]
530 fn test_get_address_invalid_address() {
531 let value = serde_json::json!("not_an_address");
532 let result = get_address(&value);
533 assert!(result.is_err());
534 assert!(result.unwrap_err().to_string().contains("Invalid address"));
535 }
536
537 #[test]
538 fn test_get_bytes_null() {
539 let value = serde_json::Value::Null;
540 let result = get_bytes(&value).unwrap();
541 assert_eq!(result.len(), 0);
542 }
543
544 #[test]
545 fn test_get_bytes_hex_string() {
546 let value = serde_json::json!("0x1234");
547 let result = get_bytes(&value).unwrap();
548 assert_eq!(result.len(), 2);
549 assert_eq!(result[0], 0x12);
550 assert_eq!(result[1], 0x34);
551 }
552
553 #[test]
554 fn test_get_bytes_invalid_type() {
555 let value = serde_json::json!(123);
556 let result = get_bytes(&value);
557 assert!(result.is_err());
558 assert!(result.unwrap_err().to_string().contains("Expected string"));
559 }
560
561 #[test]
562 fn test_get_u256_from_hex_string() {
563 let value = serde_json::json!("0x64");
564 let result = get_u256(&value).unwrap();
565 assert_eq!(result, U256::from(100));
566 }
567
568 #[test]
569 fn test_get_u256_from_number() {
570 let value = serde_json::json!(42);
571 let result = get_u256(&value).unwrap();
572 assert_eq!(result, U256::from(42));
573 }
574
575 #[test]
576 fn test_get_u256_invalid_type() {
577 let value = serde_json::json!(true);
578 let result = get_u256(&value);
579 assert!(result.is_err());
580 assert!(result.unwrap_err().to_string().contains("Expected string or number"));
581 }
582
583 #[test]
584 fn test_json_intent_to_task_intent_complete() {
585 let intent = serde_json::json!({
586 "from": "0x0000000000000000000000000000000000000001",
587 "to": "0x0000000000000000000000000000000000000002",
588 "value": "0x64",
589 "chainId": 11155111,
590 "data": "0x1234"
591 });
592 let result = json_intent_to_task_intent(&intent).unwrap();
593 assert_eq!(
594 result.from,
595 Address::from_str("0x0000000000000000000000000000000000000001").unwrap()
596 );
597 assert_eq!(
598 result.to,
599 Address::from_str("0x0000000000000000000000000000000000000002").unwrap()
600 );
601 assert_eq!(result.value, U256::from(100));
602 assert_eq!(result.chain_id, U256::from(11155111));
603 assert_eq!(result.data, "0x1234");
604 }
605
606 #[test]
607 fn test_json_intent_to_task_intent_with_function_signature() {
608 let intent = serde_json::json!({
609 "from": "0x0000000000000000000000000000000000000001",
610 "to": "0x0000000000000000000000000000000000000002",
611 "value": 100,
612 "chainId": "0x1",
613 "data": "1234",
614 "functionSignature": "0xabcd"
615 });
616 let result = json_intent_to_task_intent(&intent).unwrap();
617 assert_eq!(result.function_signature, "0xabcd");
618 assert_eq!(result.data, "0x1234"); }
620
621 #[test]
622 fn test_json_intent_to_task_intent_without_function_signature() {
623 let intent = serde_json::json!({
624 "from": "0x0000000000000000000000000000000000000001",
625 "to": "0x0000000000000000000000000000000000000002",
626 "value": 100,
627 "chainId": 1,
628 "data": "0x1234"
629 });
630 let result = json_intent_to_task_intent(&intent).unwrap();
631 assert_eq!(result.function_signature, "");
632 }
633
634 #[test]
635 fn test_json_intent_to_task_intent_missing_from() {
636 let intent = serde_json::json!({
637 "to": "0x0000000000000000000000000000000000000002",
638 "value": 100,
639 "chainId": 1,
640 "data": "0x1234"
641 });
642 let result = json_intent_to_task_intent(&intent);
643 assert!(result.is_err());
644 assert!(result.unwrap_err().to_string().contains("Missing from"));
645 }
646
647 #[test]
648 fn test_json_intent_to_task_intent_missing_to() {
649 let intent = serde_json::json!({
650 "from": "0x0000000000000000000000000000000000000001",
651 "value": 100,
652 "chainId": 1,
653 "data": "0x1234"
654 });
655 let result = json_intent_to_task_intent(&intent);
656 assert!(result.is_err());
657 assert!(result.unwrap_err().to_string().contains("Missing to"));
658 }
659
660 #[test]
661 fn test_json_intent_to_task_intent_missing_value() {
662 let intent = serde_json::json!({
663 "from": "0x0000000000000000000000000000000000000001",
664 "to": "0x0000000000000000000000000000000000000002",
665 "chainId": 1,
666 "data": "0x1234"
667 });
668 let result = json_intent_to_task_intent(&intent);
669 assert!(result.is_err());
670 assert!(result.unwrap_err().to_string().contains("Missing value"));
671 }
672
673 #[test]
674 fn test_json_intent_to_task_intent_missing_chainid() {
675 let intent = serde_json::json!({
676 "from": "0x0000000000000000000000000000000000000001",
677 "to": "0x0000000000000000000000000000000000000002",
678 "value": 100,
679 "data": "0x1234"
680 });
681 let result = json_intent_to_task_intent(&intent);
682 assert!(result.is_err());
683 assert!(result.unwrap_err().to_string().contains("Missing chainId"));
684 }
685
686 #[test]
687 fn test_json_intent_to_task_intent_missing_data() {
688 let intent = serde_json::json!({
689 "from": "0x0000000000000000000000000000000000000001",
690 "to": "0x0000000000000000000000000000000000000002",
691 "value": 100,
692 "chainId": 1
693 });
694 let result = json_intent_to_task_intent(&intent);
695 assert!(result.is_err());
696 assert!(result.unwrap_err().to_string().contains("Missing data"));
697 }
698
699 #[test]
700 fn test_json_intent_to_task_intent_data_not_string() {
701 let intent = serde_json::json!({
702 "from": "0x0000000000000000000000000000000000000001",
703 "to": "0x0000000000000000000000000000000000000002",
704 "value": 100,
705 "chainId": 1,
706 "data": 123
707 });
708 let result = json_intent_to_task_intent(&intent);
709 assert!(result.is_err());
710 assert!(result.unwrap_err().to_string().contains("data must be a string"));
711 }
712}