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        // Testnet: same gateway serves all testnet chains (Sepolia, Base Sepolia)
71        ("stagef", 11155111 | 84532) => Ok("https://gateway.stagef.testnet.newton.xyz".to_string()),
72        ("stagef", 1) => Ok("https://gateway.stagef.newton.xyz".to_string()),
73        ("prod", 11155111 | 84532) => Ok("https://gateway.testnet.newton.xyz".to_string()),
74        ("prod", 1) => Ok("https://gateway.newton.xyz".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
105// Normalize value/chainId - handles bigint, number, or hex string
106fn 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
117// Main normalize function - takes a JSON intent and returns hex versions of chain id and value
118fn normalize_intent(intent: &serde_json::Value) -> eyre::Result<serde_json::Value> {
119    let mut normalized = intent.clone();
120
121    // Normalize value
122    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    // Normalize chainId
128    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
136// Helper functions to get values from JSON
137fn get_address(value: &Value) -> eyre::Result<Address> {
138    let s = value
139        .as_str()
140        .ok_or_else(|| eyre::eyre!("Expected string for address"))?;
141    s.parse::<Address>().map_err(|e| eyre::eyre!("Invalid address: {}", e))
142}
143
144fn get_u256(value: &Value) -> eyre::Result<U256> {
145    if let Some(s) = value.as_str() {
146        hex_to_u256(s)
147    } else if let Some(n) = value.as_u64() {
148        Ok(U256::from(n))
149    } else {
150        Err(eyre::eyre!("Expected string or number for U256"))
151    }
152}
153
154// Convert raw JSON intent to TaskIntent
155fn json_intent_to_task_intent(intent: &serde_json::Value) -> eyre::Result<TaskIntent> {
156    // Normalize value and chainId first
157    let normalized_intent = normalize_intent(intent)?;
158
159    let from = get_address(
160        normalized_intent
161            .get("from")
162            .ok_or_else(|| eyre::eyre!("Missing from"))?,
163    )?;
164    let to = get_address(normalized_intent.get("to").ok_or_else(|| eyre::eyre!("Missing to"))?)?;
165    let value = get_u256(
166        normalized_intent
167            .get("value")
168            .ok_or_else(|| eyre::eyre!("Missing value"))?,
169    )?;
170    let chain_id = get_u256(
171        normalized_intent
172            .get("chainId")
173            .ok_or_else(|| eyre::eyre!("Missing chainId"))?,
174    )?;
175
176    // Get data as hex string (with or without 0x prefix)
177    let data_str = normalized_intent
178        .get("data")
179        .ok_or_else(|| eyre::eyre!("Missing data"))?
180        .as_str()
181        .ok_or_else(|| eyre::eyre!("data must be a string"))?;
182    let data = if data_str.starts_with("0x") {
183        data_str.to_string()
184    } else {
185        format!("0x{}", data_str)
186    };
187
188    // Get function signature as hex string (with or without 0x prefix)
189    let function_signature = normalized_intent
190        .get("functionSignature")
191        .and_then(|v| v.as_str())
192        .map(|s| {
193            if s.starts_with("0x") {
194                s.to_string()
195            } else {
196                format!("0x{}", s)
197            }
198        })
199        .unwrap_or_default();
200
201    Ok(TaskIntent {
202        from,
203        to,
204        value,
205        data,
206        chain_id,
207        function_signature,
208    })
209}
210
211impl TaskCommand {
212    /// Execute the task command
213    pub async fn execute(self: Box<Self>, config: NewtonAvsConfig<NewtonCliConfig>) -> eyre::Result<()> {
214        match self.subcommand {
215            TaskSubcommand::SubmitEvaluationRequest(cmd) => {
216                // Get API key from:
217                // 1. Command line flag --api-key (highest priority)
218                // 2. API_KEY environment variable (from .env file)
219                let api_key = cmd.api_key;
220
221                info!("Reading task JSON from: {:?}", cmd.task_json);
222                let contents = std::fs::read_to_string(&cmd.task_json)
223                    .with_context(|| format!("Failed to read task JSON file: {:?}", cmd.task_json))?;
224
225                let task: serde_json::Value = serde_json::from_str(&contents)
226                    .with_context(|| format!("Failed to parse task JSON: {:?}", cmd.task_json))?;
227
228                let intent_json = task
229                    .get("intent")
230                    .ok_or_else(|| eyre::eyre!("Missing 'intent' field in task"))?;
231
232                // Convert raw JSON to TaskIntent (with normalization)
233                let task_intent = json_intent_to_task_intent(intent_json)?;
234
235                let intent_sig = task
236                    .get("intentSignature")
237                    .and_then(|v| v.as_str())
238                    .map(|s| s.to_string());
239
240                // Get policy client
241                let policy_client = get_address(
242                    task.get("policyClient")
243                        .ok_or_else(|| eyre::eyre!("Missing policyClient"))?,
244                )?;
245
246                // Get optional fields
247                let quorum_number = task.get("quorumNumber").and_then(|v| v.as_str()).map(|s| s.to_string());
248                let quorum_threshold_percentage = task
249                    .get("quorumThresholdPercentage")
250                    .and_then(|v| v.as_u64())
251                    .map(|v| v as u8);
252                let wasm_args = task.get("wasmArgs").and_then(|v| v.as_str()).map(|s| s.to_string());
253                let timeout = task.get("timeout").and_then(|v| v.as_u64());
254
255                // Build CreateTaskRequest for Gateway
256                let request = CreateTaskRequest {
257                    policy_client,
258                    intent: task_intent,
259                    intent_signature: intent_sig,
260                    quorum_number,
261                    quorum_threshold_percentage,
262                    wasm_args,
263                    timeout,
264                    use_two_phase: None,
265                    encrypted_data_refs: None,
266                    user_signature: None,
267                    app_signature: None,
268                    proof_cid: task.get("proofCid").and_then(|v| v.as_str()).map(|s| s.to_string()),
269                    user_pubkey: None,
270                    app_pubkey: None,
271                    include_validate_calldata: None,
272                };
273
274                // Serialize to JSON for JSON-RPC request
275                let request_json = serde_json::to_value(&request).with_context(|| "Failed to serialize request")?;
276
277                let payload =
278                    create_json_rpc_request_payload("newt_createTask", serde_json::Value::Array(vec![request_json]));
279
280                let chain_id = config.chain_id;
281                let deployment_env = std::env::var("DEPLOYMENT_ENV").unwrap_or_else(|_| "prod".to_string());
282                let gateway_url = get_gateway_url(chain_id, &deployment_env)?;
283
284                info!("Submitting evaluation request to: {}", gateway_url);
285                let response = http_post(&gateway_url, &payload, api_key.as_deref()).await?;
286
287                info!("Response: {}", serde_json::to_string_pretty(&response)?);
288
289                Ok(())
290            }
291        }
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298    use std::str::FromStr;
299
300    #[test]
301    fn test_get_next_id() {
302        // Reset counter by creating new instances (note: this is a static, so we can't truly reset)
303        // Just verify it returns incrementing values
304        let id1 = get_next_id();
305        let id2 = get_next_id();
306        let id3 = get_next_id();
307        assert!(id2 > id1);
308        assert!(id3 > id2);
309    }
310
311    #[test]
312    fn test_create_json_rpc_request_payload() {
313        let method = "test_method";
314        let params = serde_json::json!({"key": "value"});
315        let payload = create_json_rpc_request_payload(method, params.clone());
316
317        assert_eq!(payload["jsonrpc"], "2.0");
318        assert_eq!(payload["method"], method);
319        assert_eq!(payload["params"], params);
320        assert!(payload["id"].is_number());
321    }
322
323    #[test]
324    fn test_hex_to_u256_with_prefix() {
325        let result = hex_to_u256("0x1a2b").unwrap();
326        assert_eq!(result, U256::from(0x1a2b));
327    }
328
329    #[test]
330    fn test_hex_to_u256_without_prefix() {
331        let result = hex_to_u256("1a2b").unwrap();
332        assert_eq!(result, U256::from(0x1a2b));
333    }
334
335    #[test]
336    fn test_hex_to_u256_large_value() {
337        let large_hex = "0xffffffffffffffffffffffffffffffff";
338        let result = hex_to_u256(large_hex).unwrap();
339        assert!(result > U256::from(u64::MAX));
340    }
341
342    #[test]
343    fn test_hex_to_u256_invalid_hex() {
344        let result = hex_to_u256("0xinvalid");
345        assert!(result.is_err());
346        assert!(result.unwrap_err().to_string().contains("Failed to parse hex string"));
347    }
348
349    #[test]
350    fn test_hex_to_u256_empty_string() {
351        let result = hex_to_u256("");
352        // Empty string should fail or return 0
353        assert!(result.is_ok() || result.is_err());
354    }
355
356    #[test]
357    fn test_get_gateway_url_stagef_sepolia() {
358        let url = get_gateway_url(11155111, "stagef").unwrap();
359        assert_eq!(url, "https://gateway.stagef.testnet.newton.xyz");
360    }
361
362    #[test]
363    fn test_get_gateway_url_stagef_mainnet() {
364        let url = get_gateway_url(1, "stagef").unwrap();
365        assert_eq!(url, "https://gateway.stagef.newton.xyz");
366    }
367
368    #[test]
369    fn test_get_gateway_url_prod_sepolia() {
370        let url = get_gateway_url(11155111, "prod").unwrap();
371        assert_eq!(url, "https://gateway.testnet.newton.xyz");
372    }
373
374    #[test]
375    fn test_get_gateway_url_prod_mainnet() {
376        let url = get_gateway_url(1, "prod").unwrap();
377        assert_eq!(url, "https://gateway.newton.xyz");
378    }
379
380    #[test]
381    fn test_get_gateway_url_stagef_base_sepolia() {
382        let url = get_gateway_url(84532, "stagef").unwrap();
383        assert_eq!(url, "https://gateway.stagef.testnet.newton.xyz");
384    }
385
386    #[test]
387    fn test_get_gateway_url_prod_base_sepolia() {
388        let url = get_gateway_url(84532, "prod").unwrap();
389        assert_eq!(url, "https://gateway.testnet.newton.xyz");
390    }
391
392    #[test]
393    fn test_get_gateway_url_unsupported() {
394        let result = get_gateway_url(999, "stagef");
395        assert!(result.is_err());
396        assert!(result.unwrap_err().to_string().contains("Unsupported combination"));
397    }
398
399    #[test]
400    fn test_normalize_to_u256_from_hex_string() {
401        let value = serde_json::json!("0x64");
402        let result = normalize_to_u256(&value).unwrap();
403        assert_eq!(result, U256::from(100));
404    }
405
406    #[test]
407    fn test_normalize_to_u256_from_decimal_string() {
408        // Note: normalize_to_u256 only handles hex strings (via hex_to_u256), not decimal strings
409        // A decimal string like "100" will be interpreted as hex (which is 256 in decimal)
410        let value = serde_json::json!("100");
411        let result = normalize_to_u256(&value).unwrap();
412        // "100" as hex = 0x100 = 256 in decimal
413        assert_eq!(result, U256::from(256));
414    }
415
416    #[test]
417    fn test_normalize_to_u256_from_number() {
418        let value = serde_json::json!(42);
419        let result = normalize_to_u256(&value).unwrap();
420        assert_eq!(result, U256::from(42));
421    }
422
423    #[test]
424    fn test_normalize_to_u256_invalid_type() {
425        let value = serde_json::json!(true);
426        let result = normalize_to_u256(&value);
427        assert!(result.is_err());
428        assert!(result.unwrap_err().to_string().contains("Invalid value type"));
429    }
430
431    #[test]
432    fn test_normalize_intent_with_value_and_chainid() {
433        let intent = serde_json::json!({
434            "value": "0x64",
435            "chainId": 11155111,
436            "from": "0x0000000000000000000000000000000000000001",
437            "to": "0x0000000000000000000000000000000000000002"
438        });
439        let result = normalize_intent(&intent).unwrap();
440        assert_eq!(result["value"], "0x64");
441        assert_eq!(result["chainId"], "0xaa36a7"); // 11155111 in hex
442    }
443
444    #[test]
445    fn test_normalize_intent_with_number_value() {
446        let intent = serde_json::json!({
447            "value": 100,
448            "chainId": "0x1",
449            "from": "0x0000000000000000000000000000000000000001",
450            "to": "0x0000000000000000000000000000000000000002"
451        });
452        let result = normalize_intent(&intent).unwrap();
453        assert_eq!(result["value"], "0x64"); // 100 in hex
454        assert_eq!(result["chainId"], "0x1");
455    }
456
457    #[test]
458    fn test_normalize_intent_without_value_or_chainid() {
459        let intent = serde_json::json!({
460            "from": "0x0000000000000000000000000000000000000001",
461            "to": "0x0000000000000000000000000000000000000002"
462        });
463        let result = normalize_intent(&intent).unwrap();
464        assert_eq!(result["from"], "0x0000000000000000000000000000000000000001");
465        assert_eq!(result["to"], "0x0000000000000000000000000000000000000002");
466    }
467
468    #[test]
469    fn test_normalize_intent_invalid_value() {
470        let intent = serde_json::json!({
471            "value": "invalid",
472            "chainId": 1
473        });
474        let result = normalize_intent(&intent);
475        assert!(result.is_err());
476    }
477
478    #[test]
479    fn test_get_address_valid() {
480        let value = serde_json::json!("0x0000000000000000000000000000000000000001");
481        let result = get_address(&value).unwrap();
482        assert_eq!(
483            result,
484            Address::from_str("0x0000000000000000000000000000000000000001").unwrap()
485        );
486    }
487
488    #[test]
489    fn test_get_address_invalid_type() {
490        let value = serde_json::json!(123);
491        let result = get_address(&value);
492        assert!(result.is_err());
493        assert!(result.unwrap_err().to_string().contains("Expected string"));
494    }
495
496    #[test]
497    fn test_get_address_invalid_address() {
498        let value = serde_json::json!("not_an_address");
499        let result = get_address(&value);
500        assert!(result.is_err());
501        assert!(result.unwrap_err().to_string().contains("Invalid address"));
502    }
503
504    #[test]
505    fn test_get_u256_from_hex_string() {
506        let value = serde_json::json!("0x64");
507        let result = get_u256(&value).unwrap();
508        assert_eq!(result, U256::from(100));
509    }
510
511    #[test]
512    fn test_get_u256_from_number() {
513        let value = serde_json::json!(42);
514        let result = get_u256(&value).unwrap();
515        assert_eq!(result, U256::from(42));
516    }
517
518    #[test]
519    fn test_get_u256_invalid_type() {
520        let value = serde_json::json!(true);
521        let result = get_u256(&value);
522        assert!(result.is_err());
523        assert!(result.unwrap_err().to_string().contains("Expected string or number"));
524    }
525
526    #[test]
527    fn test_json_intent_to_task_intent_complete() {
528        let intent = serde_json::json!({
529            "from": "0x0000000000000000000000000000000000000001",
530            "to": "0x0000000000000000000000000000000000000002",
531            "value": "0x64",
532            "chainId": 11155111,
533            "data": "0x1234"
534        });
535        let result = json_intent_to_task_intent(&intent).unwrap();
536        assert_eq!(
537            result.from,
538            Address::from_str("0x0000000000000000000000000000000000000001").unwrap()
539        );
540        assert_eq!(
541            result.to,
542            Address::from_str("0x0000000000000000000000000000000000000002").unwrap()
543        );
544        assert_eq!(result.value, U256::from(100));
545        assert_eq!(result.chain_id, U256::from(11155111));
546        assert_eq!(result.data, "0x1234");
547    }
548
549    #[test]
550    fn test_json_intent_to_task_intent_with_function_signature() {
551        let intent = serde_json::json!({
552            "from": "0x0000000000000000000000000000000000000001",
553            "to": "0x0000000000000000000000000000000000000002",
554            "value": 100,
555            "chainId": "0x1",
556            "data": "1234",
557            "functionSignature": "0xabcd"
558        });
559        let result = json_intent_to_task_intent(&intent).unwrap();
560        assert_eq!(result.function_signature, "0xabcd");
561        assert_eq!(result.data, "0x1234"); // Should add 0x prefix
562    }
563
564    #[test]
565    fn test_json_intent_to_task_intent_without_function_signature() {
566        let intent = serde_json::json!({
567            "from": "0x0000000000000000000000000000000000000001",
568            "to": "0x0000000000000000000000000000000000000002",
569            "value": 100,
570            "chainId": 1,
571            "data": "0x1234"
572        });
573        let result = json_intent_to_task_intent(&intent).unwrap();
574        assert_eq!(result.function_signature, "");
575    }
576
577    #[test]
578    fn test_json_intent_to_task_intent_missing_from() {
579        let intent = serde_json::json!({
580            "to": "0x0000000000000000000000000000000000000002",
581            "value": 100,
582            "chainId": 1,
583            "data": "0x1234"
584        });
585        let result = json_intent_to_task_intent(&intent);
586        assert!(result.is_err());
587        assert!(result.unwrap_err().to_string().contains("Missing from"));
588    }
589
590    #[test]
591    fn test_json_intent_to_task_intent_missing_to() {
592        let intent = serde_json::json!({
593            "from": "0x0000000000000000000000000000000000000001",
594            "value": 100,
595            "chainId": 1,
596            "data": "0x1234"
597        });
598        let result = json_intent_to_task_intent(&intent);
599        assert!(result.is_err());
600        assert!(result.unwrap_err().to_string().contains("Missing to"));
601    }
602
603    #[test]
604    fn test_json_intent_to_task_intent_missing_value() {
605        let intent = serde_json::json!({
606            "from": "0x0000000000000000000000000000000000000001",
607            "to": "0x0000000000000000000000000000000000000002",
608            "chainId": 1,
609            "data": "0x1234"
610        });
611        let result = json_intent_to_task_intent(&intent);
612        assert!(result.is_err());
613        assert!(result.unwrap_err().to_string().contains("Missing value"));
614    }
615
616    #[test]
617    fn test_json_intent_to_task_intent_missing_chainid() {
618        let intent = serde_json::json!({
619            "from": "0x0000000000000000000000000000000000000001",
620            "to": "0x0000000000000000000000000000000000000002",
621            "value": 100,
622            "data": "0x1234"
623        });
624        let result = json_intent_to_task_intent(&intent);
625        assert!(result.is_err());
626        assert!(result.unwrap_err().to_string().contains("Missing chainId"));
627    }
628
629    #[test]
630    fn test_json_intent_to_task_intent_missing_data() {
631        let intent = serde_json::json!({
632            "from": "0x0000000000000000000000000000000000000001",
633            "to": "0x0000000000000000000000000000000000000002",
634            "value": 100,
635            "chainId": 1
636        });
637        let result = json_intent_to_task_intent(&intent);
638        assert!(result.is_err());
639        assert!(result.unwrap_err().to_string().contains("Missing data"));
640    }
641
642    #[test]
643    fn test_json_intent_to_task_intent_data_not_string() {
644        let intent = serde_json::json!({
645            "from": "0x0000000000000000000000000000000000000001",
646            "to": "0x0000000000000000000000000000000000000002",
647            "value": 100,
648            "chainId": 1,
649            "data": 123
650        });
651        let result = json_intent_to_task_intent(&intent);
652        assert!(result.is_err());
653        assert!(result.unwrap_err().to_string().contains("data must be a string"));
654    }
655}