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 #[arg(long, env = "API_KEY")]
44 api_key: Option<String>,
45}
46
47static NEXT_ID: AtomicU64 = AtomicU64::new(0);
49
50fn get_next_id() -> u64 {
51 NEXT_ID.fetch_add(1, Ordering::Relaxed) + 1
52}
53
54fn create_json_rpc_request_payload(method: &str, params: serde_json::Value) -> serde_json::Value {
55 serde_json::json!({
56 "jsonrpc": "2.0",
57 "id": get_next_id(),
58 "method": method,
59 "params": params,
60 })
61}
62
63fn hex_to_u256(hex_str: &str) -> eyre::Result<U256> {
65 let hex_str = hex_str.strip_prefix("0x").unwrap_or(hex_str);
66 U256::from_str_radix(hex_str, 16).map_err(|e| eyre::eyre!("Failed to parse hex string '{}': {}", hex_str, e))
67}
68
69fn get_prover_avs_url(chain_id: u64, deployment_env: &str) -> eyre::Result<String> {
70 match (deployment_env, chain_id) {
71 ("stagef", 11155111) => Ok("https://prover-avs.stagef.sepolia.newt.foundation".to_string()),
72 ("stagef", 1) => Ok("https://prover-avs.stagef.newt.foundation".to_string()),
73 ("prod", 11155111) => Ok("https://prover-avs.sepolia.newt.foundation".to_string()),
74 ("prod", 1) => Ok("https://prover-avs.newt.foundation".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 hex_to_bytes(hex_str: &str) -> eyre::Result<Bytes> {
138 let hex_str = hex_str.strip_prefix("0x").unwrap_or(hex_str);
139 let bytes = hex::decode(hex_str).map_err(|e| eyre::eyre!("Failed to decode hex: {}", e))?;
140 Ok(Bytes::copy_from_slice(&bytes))
141}
142
143fn get_address(value: &Value) -> eyre::Result<Address> {
145 let s = value
146 .as_str()
147 .ok_or_else(|| eyre::eyre!("Expected string for address"))?;
148 s.parse::<Address>().map_err(|e| eyre::eyre!("Invalid address: {}", e))
149}
150
151fn get_bytes(value: &Value) -> eyre::Result<Bytes> {
152 if value.is_null() {
153 return Ok(Bytes::new());
154 }
155 if let Some(s) = value.as_str() {
156 hex_to_bytes(s)
157 } else {
158 Err(eyre::eyre!("Expected string for bytes"))
159 }
160}
161
162fn get_u256(value: &Value) -> eyre::Result<U256> {
163 if let Some(s) = value.as_str() {
164 hex_to_u256(s)
165 } else if let Some(n) = value.as_u64() {
166 Ok(U256::from(n))
167 } else {
168 Err(eyre::eyre!("Expected string or number for U256"))
169 }
170}
171
172fn json_intent_to_task_intent(intent: &serde_json::Value) -> eyre::Result<TaskIntent> {
174 let normalized_intent = normalize_intent(intent)?;
176
177 let from = get_address(
178 normalized_intent
179 .get("from")
180 .ok_or_else(|| eyre::eyre!("Missing from"))?,
181 )?;
182 let to = get_address(normalized_intent.get("to").ok_or_else(|| eyre::eyre!("Missing to"))?)?;
183 let value = get_u256(
184 normalized_intent
185 .get("value")
186 .ok_or_else(|| eyre::eyre!("Missing value"))?,
187 )?;
188 let chain_id = get_u256(
189 normalized_intent
190 .get("chainId")
191 .ok_or_else(|| eyre::eyre!("Missing chainId"))?,
192 )?;
193
194 let data_str = normalized_intent
196 .get("data")
197 .ok_or_else(|| eyre::eyre!("Missing data"))?
198 .as_str()
199 .ok_or_else(|| eyre::eyre!("data must be a string"))?;
200 let data = if data_str.starts_with("0x") {
201 data_str.to_string()
202 } else {
203 format!("0x{}", data_str)
204 };
205
206 let function_signature = normalized_intent
208 .get("functionSignature")
209 .and_then(|v| v.as_str())
210 .map(|s| {
211 if s.starts_with("0x") {
212 s.to_string()
213 } else {
214 format!("0x{}", s)
215 }
216 })
217 .unwrap_or_default();
218
219 Ok(TaskIntent {
220 from,
221 to,
222 value,
223 data,
224 chain_id,
225 function_signature,
226 })
227}
228
229impl TaskCommand {
230 pub async fn execute(self: Box<Self>, config: NewtonAvsConfig<NewtonCliConfig>) -> eyre::Result<()> {
232 match self.subcommand {
233 TaskSubcommand::SubmitEvaluationRequest(cmd) => {
234 let private_key = cmd
235 .private_key
236 .ok_or_else(|| eyre::eyre!("PRIVATE_KEY is required (set via --private-key or env var)"))?;
237
238 let api_key = cmd.api_key.or_else(|| config.service.user_secret_key.clone());
243
244 info!("Reading task JSON from: {:?}", cmd.task_json);
245 let contents = std::fs::read_to_string(&cmd.task_json)
246 .with_context(|| format!("Failed to read task JSON file: {:?}", cmd.task_json))?;
247
248 let task: serde_json::Value = serde_json::from_str(&contents)
249 .with_context(|| format!("Failed to parse task JSON: {:?}", cmd.task_json))?;
250
251 let intent_json = task
252 .get("intent")
253 .ok_or_else(|| eyre::eyre!("Missing 'intent' field in task"))?;
254
255 let task_intent = json_intent_to_task_intent(intent_json)?;
257
258 let i_task_intent = ITaskIntent::try_from(&task_intent)
260 .with_context(|| "Failed to convert TaskIntent to ITaskIntent")?;
261
262 let intent_sig = task
263 .get("intentSignature")
264 .map(get_bytes)
265 .transpose()?
266 .unwrap_or_default();
267
268 let policy_client = get_address(
270 task.get("policyClient")
271 .ok_or_else(|| eyre::eyre!("Missing policyClient"))?,
272 )?;
273
274 let quorum_number = task.get("quorumNumber").map(get_bytes).transpose()?.unwrap_or_default();
276 let quorum_threshold_percentage = task
277 .get("quorumThresholdPercentage")
278 .and_then(|v| v.as_u64())
279 .map(|v| v as u32)
280 .unwrap_or(0);
281 let wasm_args = task.get("wasmArgs").map(get_bytes).transpose()?.unwrap_or_default();
282 let timeout = task.get("timeout").and_then(|v| v.as_u64()).unwrap_or(60);
283
284 let i_request = ICreateTaskAndWaitRequest {
286 policyClient: policy_client,
287 intent: i_task_intent,
288 intentSignature: intent_sig,
289 quorumNumber: quorum_number,
290 quorumThresholdPercentage: quorum_threshold_percentage,
291 wasmArgs: wasm_args,
292 timeout,
293 };
294
295 let signed_request = CreateTaskAndWaitRequest::signed_from(&i_request, &private_key);
297
298 let signed_request_json =
300 serde_json::to_value(&signed_request).with_context(|| "Failed to serialize signed request")?;
301
302 let payload = create_json_rpc_request_payload(
303 "newton_createTaskAndWait",
304 serde_json::Value::Array(vec![signed_request_json]),
305 );
306
307 let chain_id = config.chain_id;
308 let deployment_env = std::env::var("DEPLOYMENT_ENV").unwrap_or_else(|_| "prod".to_string());
309 let prover_avs_url = get_prover_avs_url(chain_id, &deployment_env)?;
310
311 info!("Submitting evaluation request to: {}", prover_avs_url);
312 let response = http_post(&prover_avs_url, &payload, api_key.as_deref()).await?;
313
314 info!("Response: {}", serde_json::to_string_pretty(&response)?);
315
316 Ok(())
317 }
318 }
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325 use std::str::FromStr;
326
327 #[test]
328 fn test_get_next_id() {
329 let id1 = get_next_id();
332 let id2 = get_next_id();
333 let id3 = get_next_id();
334 assert!(id2 > id1);
335 assert!(id3 > id2);
336 }
337
338 #[test]
339 fn test_create_json_rpc_request_payload() {
340 let method = "test_method";
341 let params = serde_json::json!({"key": "value"});
342 let payload = create_json_rpc_request_payload(method, params.clone());
343
344 assert_eq!(payload["jsonrpc"], "2.0");
345 assert_eq!(payload["method"], method);
346 assert_eq!(payload["params"], params);
347 assert!(payload["id"].is_number());
348 }
349
350 #[test]
351 fn test_hex_to_u256_with_prefix() {
352 let result = hex_to_u256("0x1a2b").unwrap();
353 assert_eq!(result, U256::from(0x1a2b));
354 }
355
356 #[test]
357 fn test_hex_to_u256_without_prefix() {
358 let result = hex_to_u256("1a2b").unwrap();
359 assert_eq!(result, U256::from(0x1a2b));
360 }
361
362 #[test]
363 fn test_hex_to_u256_large_value() {
364 let large_hex = "0xffffffffffffffffffffffffffffffff";
365 let result = hex_to_u256(large_hex).unwrap();
366 assert!(result > U256::from(u64::MAX));
367 }
368
369 #[test]
370 fn test_hex_to_u256_invalid_hex() {
371 let result = hex_to_u256("0xinvalid");
372 assert!(result.is_err());
373 assert!(result.unwrap_err().to_string().contains("Failed to parse hex string"));
374 }
375
376 #[test]
377 fn test_hex_to_u256_empty_string() {
378 let result = hex_to_u256("");
379 assert!(result.is_ok() || result.is_err());
381 }
382
383 #[test]
384 fn test_get_prover_avs_url_stagef_sepolia() {
385 let url = get_prover_avs_url(11155111, "stagef").unwrap();
386 assert_eq!(url, "https://prover-avs.stagef.sepolia.newt.foundation");
387 }
388
389 #[test]
390 fn test_get_prover_avs_url_stagef_mainnet() {
391 let url = get_prover_avs_url(1, "stagef").unwrap();
392 assert_eq!(url, "https://prover-avs.stagef.newt.foundation");
393 }
394
395 #[test]
396 fn test_get_prover_avs_url_prod_sepolia() {
397 let url = get_prover_avs_url(11155111, "prod").unwrap();
398 assert_eq!(url, "https://prover-avs.sepolia.newt.foundation");
399 }
400
401 #[test]
402 fn test_get_prover_avs_url_prod_mainnet() {
403 let url = get_prover_avs_url(1, "prod").unwrap();
404 assert_eq!(url, "https://prover-avs.newt.foundation");
405 }
406
407 #[test]
408 fn test_get_prover_avs_url_unsupported() {
409 let result = get_prover_avs_url(999, "stagef");
410 assert!(result.is_err());
411 assert!(result.unwrap_err().to_string().contains("Unsupported combination"));
412 }
413
414 #[test]
415 fn test_normalize_to_u256_from_hex_string() {
416 let value = serde_json::json!("0x64");
417 let result = normalize_to_u256(&value).unwrap();
418 assert_eq!(result, U256::from(100));
419 }
420
421 #[test]
422 fn test_normalize_to_u256_from_decimal_string() {
423 let value = serde_json::json!("100");
426 let result = normalize_to_u256(&value).unwrap();
427 assert_eq!(result, U256::from(256));
429 }
430
431 #[test]
432 fn test_normalize_to_u256_from_number() {
433 let value = serde_json::json!(42);
434 let result = normalize_to_u256(&value).unwrap();
435 assert_eq!(result, U256::from(42));
436 }
437
438 #[test]
439 fn test_normalize_to_u256_invalid_type() {
440 let value = serde_json::json!(true);
441 let result = normalize_to_u256(&value);
442 assert!(result.is_err());
443 assert!(result.unwrap_err().to_string().contains("Invalid value type"));
444 }
445
446 #[test]
447 fn test_normalize_intent_with_value_and_chainid() {
448 let intent = serde_json::json!({
449 "value": "0x64",
450 "chainId": 11155111,
451 "from": "0x0000000000000000000000000000000000000001",
452 "to": "0x0000000000000000000000000000000000000002"
453 });
454 let result = normalize_intent(&intent).unwrap();
455 assert_eq!(result["value"], "0x64");
456 assert_eq!(result["chainId"], "0xaa36a7"); }
458
459 #[test]
460 fn test_normalize_intent_with_number_value() {
461 let intent = serde_json::json!({
462 "value": 100,
463 "chainId": "0x1",
464 "from": "0x0000000000000000000000000000000000000001",
465 "to": "0x0000000000000000000000000000000000000002"
466 });
467 let result = normalize_intent(&intent).unwrap();
468 assert_eq!(result["value"], "0x64"); assert_eq!(result["chainId"], "0x1");
470 }
471
472 #[test]
473 fn test_normalize_intent_without_value_or_chainid() {
474 let intent = serde_json::json!({
475 "from": "0x0000000000000000000000000000000000000001",
476 "to": "0x0000000000000000000000000000000000000002"
477 });
478 let result = normalize_intent(&intent).unwrap();
479 assert_eq!(result["from"], "0x0000000000000000000000000000000000000001");
480 assert_eq!(result["to"], "0x0000000000000000000000000000000000000002");
481 }
482
483 #[test]
484 fn test_normalize_intent_invalid_value() {
485 let intent = serde_json::json!({
486 "value": "invalid",
487 "chainId": 1
488 });
489 let result = normalize_intent(&intent);
490 assert!(result.is_err());
491 }
492
493 #[test]
494 fn test_hex_to_bytes_with_prefix() {
495 let result = hex_to_bytes("0x1234").unwrap();
496 assert_eq!(result.len(), 2);
497 assert_eq!(result[0], 0x12);
498 assert_eq!(result[1], 0x34);
499 }
500
501 #[test]
502 fn test_hex_to_bytes_without_prefix() {
503 let result = hex_to_bytes("1234").unwrap();
504 assert_eq!(result.len(), 2);
505 assert_eq!(result[0], 0x12);
506 assert_eq!(result[1], 0x34);
507 }
508
509 #[test]
510 fn test_hex_to_bytes_empty() {
511 let result = hex_to_bytes("0x").unwrap();
512 assert_eq!(result.len(), 0);
513 }
514
515 #[test]
516 fn test_hex_to_bytes_invalid_hex() {
517 let result = hex_to_bytes("0xinvalid");
518 assert!(result.is_err());
519 assert!(result.unwrap_err().to_string().contains("Failed to decode hex"));
520 }
521
522 #[test]
523 fn test_get_address_valid() {
524 let value = serde_json::json!("0x0000000000000000000000000000000000000001");
525 let result = get_address(&value).unwrap();
526 assert_eq!(
527 result,
528 Address::from_str("0x0000000000000000000000000000000000000001").unwrap()
529 );
530 }
531
532 #[test]
533 fn test_get_address_invalid_type() {
534 let value = serde_json::json!(123);
535 let result = get_address(&value);
536 assert!(result.is_err());
537 assert!(result.unwrap_err().to_string().contains("Expected string"));
538 }
539
540 #[test]
541 fn test_get_address_invalid_address() {
542 let value = serde_json::json!("not_an_address");
543 let result = get_address(&value);
544 assert!(result.is_err());
545 assert!(result.unwrap_err().to_string().contains("Invalid address"));
546 }
547
548 #[test]
549 fn test_get_bytes_null() {
550 let value = serde_json::Value::Null;
551 let result = get_bytes(&value).unwrap();
552 assert_eq!(result.len(), 0);
553 }
554
555 #[test]
556 fn test_get_bytes_hex_string() {
557 let value = serde_json::json!("0x1234");
558 let result = get_bytes(&value).unwrap();
559 assert_eq!(result.len(), 2);
560 assert_eq!(result[0], 0x12);
561 assert_eq!(result[1], 0x34);
562 }
563
564 #[test]
565 fn test_get_bytes_invalid_type() {
566 let value = serde_json::json!(123);
567 let result = get_bytes(&value);
568 assert!(result.is_err());
569 assert!(result.unwrap_err().to_string().contains("Expected string"));
570 }
571
572 #[test]
573 fn test_get_u256_from_hex_string() {
574 let value = serde_json::json!("0x64");
575 let result = get_u256(&value).unwrap();
576 assert_eq!(result, U256::from(100));
577 }
578
579 #[test]
580 fn test_get_u256_from_number() {
581 let value = serde_json::json!(42);
582 let result = get_u256(&value).unwrap();
583 assert_eq!(result, U256::from(42));
584 }
585
586 #[test]
587 fn test_get_u256_invalid_type() {
588 let value = serde_json::json!(true);
589 let result = get_u256(&value);
590 assert!(result.is_err());
591 assert!(result.unwrap_err().to_string().contains("Expected string or number"));
592 }
593
594 #[test]
595 fn test_json_intent_to_task_intent_complete() {
596 let intent = serde_json::json!({
597 "from": "0x0000000000000000000000000000000000000001",
598 "to": "0x0000000000000000000000000000000000000002",
599 "value": "0x64",
600 "chainId": 11155111,
601 "data": "0x1234"
602 });
603 let result = json_intent_to_task_intent(&intent).unwrap();
604 assert_eq!(
605 result.from,
606 Address::from_str("0x0000000000000000000000000000000000000001").unwrap()
607 );
608 assert_eq!(
609 result.to,
610 Address::from_str("0x0000000000000000000000000000000000000002").unwrap()
611 );
612 assert_eq!(result.value, U256::from(100));
613 assert_eq!(result.chain_id, U256::from(11155111));
614 assert_eq!(result.data, "0x1234");
615 }
616
617 #[test]
618 fn test_json_intent_to_task_intent_with_function_signature() {
619 let intent = serde_json::json!({
620 "from": "0x0000000000000000000000000000000000000001",
621 "to": "0x0000000000000000000000000000000000000002",
622 "value": 100,
623 "chainId": "0x1",
624 "data": "1234",
625 "functionSignature": "0xabcd"
626 });
627 let result = json_intent_to_task_intent(&intent).unwrap();
628 assert_eq!(result.function_signature, "0xabcd");
629 assert_eq!(result.data, "0x1234"); }
631
632 #[test]
633 fn test_json_intent_to_task_intent_without_function_signature() {
634 let intent = serde_json::json!({
635 "from": "0x0000000000000000000000000000000000000001",
636 "to": "0x0000000000000000000000000000000000000002",
637 "value": 100,
638 "chainId": 1,
639 "data": "0x1234"
640 });
641 let result = json_intent_to_task_intent(&intent).unwrap();
642 assert_eq!(result.function_signature, "");
643 }
644
645 #[test]
646 fn test_json_intent_to_task_intent_missing_from() {
647 let intent = serde_json::json!({
648 "to": "0x0000000000000000000000000000000000000002",
649 "value": 100,
650 "chainId": 1,
651 "data": "0x1234"
652 });
653 let result = json_intent_to_task_intent(&intent);
654 assert!(result.is_err());
655 assert!(result.unwrap_err().to_string().contains("Missing from"));
656 }
657
658 #[test]
659 fn test_json_intent_to_task_intent_missing_to() {
660 let intent = serde_json::json!({
661 "from": "0x0000000000000000000000000000000000000001",
662 "value": 100,
663 "chainId": 1,
664 "data": "0x1234"
665 });
666 let result = json_intent_to_task_intent(&intent);
667 assert!(result.is_err());
668 assert!(result.unwrap_err().to_string().contains("Missing to"));
669 }
670
671 #[test]
672 fn test_json_intent_to_task_intent_missing_value() {
673 let intent = serde_json::json!({
674 "from": "0x0000000000000000000000000000000000000001",
675 "to": "0x0000000000000000000000000000000000000002",
676 "chainId": 1,
677 "data": "0x1234"
678 });
679 let result = json_intent_to_task_intent(&intent);
680 assert!(result.is_err());
681 assert!(result.unwrap_err().to_string().contains("Missing value"));
682 }
683
684 #[test]
685 fn test_json_intent_to_task_intent_missing_chainid() {
686 let intent = serde_json::json!({
687 "from": "0x0000000000000000000000000000000000000001",
688 "to": "0x0000000000000000000000000000000000000002",
689 "value": 100,
690 "data": "0x1234"
691 });
692 let result = json_intent_to_task_intent(&intent);
693 assert!(result.is_err());
694 assert!(result.unwrap_err().to_string().contains("Missing chainId"));
695 }
696
697 #[test]
698 fn test_json_intent_to_task_intent_missing_data() {
699 let intent = serde_json::json!({
700 "from": "0x0000000000000000000000000000000000000001",
701 "to": "0x0000000000000000000000000000000000000002",
702 "value": 100,
703 "chainId": 1
704 });
705 let result = json_intent_to_task_intent(&intent);
706 assert!(result.is_err());
707 assert!(result.unwrap_err().to_string().contains("Missing data"));
708 }
709
710 #[test]
711 fn test_json_intent_to_task_intent_data_not_string() {
712 let intent = serde_json::json!({
713 "from": "0x0000000000000000000000000000000000000001",
714 "to": "0x0000000000000000000000000000000000000002",
715 "value": 100,
716 "chainId": 1,
717 "data": 123
718 });
719 let result = json_intent_to_task_intent(&intent);
720 assert!(result.is_err());
721 assert!(result.unwrap_err().to_string().contains("data must be a string"));
722 }
723}