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