Skip to main content

newton_cli/commands/
task.rs

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/// Task commands
17#[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    /// Submit evaluation request to prover AVS
27    #[command(name = "submit-evaluation-request")]
28    SubmitEvaluationRequest(SubmitEvaluationRequestCommand),
29}
30
31/// Submit evaluation request command
32#[derive(Debug, Parser)]
33pub struct SubmitEvaluationRequestCommand {
34    /// Path to task JSON file
35    #[arg(long)]
36    task_json: PathBuf,
37
38    #[arg(long, env = "PRIVATE_KEY")]
39    private_key: Option<String>,
40
41    /// API key for authentication
42    #[arg(long, env = "API_KEY")]
43    api_key: Option<String>,
44}
45
46// Static counter for JSON-RPC request IDs
47static 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
62// Helper function to convert hex string to U256
63fn 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
104// Normalize value/chainId - handles bigint, number, or hex string
105fn 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
116// Main normalize function - takes a JSON intent and returns hex versions of chain id and value
117fn normalize_intent(intent: &serde_json::Value) -> eyre::Result<serde_json::Value> {
118    let mut normalized = intent.clone();
119
120    // Normalize value
121    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    // Normalize chainId
127    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
135// Helper functions to get values from JSON
136fn 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
153// Convert raw JSON intent to TaskIntent
154fn json_intent_to_task_intent(intent: &serde_json::Value) -> eyre::Result<TaskIntent> {
155    // Normalize value and chainId first
156    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    // Get data as hex string (with or without 0x prefix)
176    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    // Get function signature as hex string (with or without 0x prefix)
188    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    /// Execute the task command
212    pub async fn execute(self: Box<Self>, config: NewtonAvsConfig<NewtonCliConfig>) -> eyre::Result<()> {
213        match self.subcommand {
214            TaskSubcommand::SubmitEvaluationRequest(cmd) => {
215                // Get API key from:
216                // 1. Command line flag --api-key (highest priority)
217                // 2. API_KEY environment variable (from .env file)
218                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                // Convert raw JSON to TaskIntent (with normalization)
232                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                // Get policy client
240                let policy_client = get_address(
241                    task.get("policyClient")
242                        .ok_or_else(|| eyre::eyre!("Missing policyClient"))?,
243                )?;
244
245                // Get optional fields
246                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                // Build CreateTaskRequest for Gateway
255                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                // Serialize to JSON for JSON-RPC request
270                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        // Reset counter by creating new instances (note: this is a static, so we can't truly reset)
298        // Just verify it returns incrementing values
299        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        // Empty string should fail or return 0
348        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        // Note: normalize_to_u256 only handles hex strings (via hex_to_u256), not decimal strings
392        // A decimal string like "100" will be interpreted as hex (which is 256 in decimal)
393        let value = serde_json::json!("100");
394        let result = normalize_to_u256(&value).unwrap();
395        // "100" as hex = 0x100 = 256 in decimal
396        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"); // 11155111 in hex
425    }
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"); // 100 in hex
437        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"); // Should add 0x prefix
545    }
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}