newton_cli/commands/
task.rs

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/// Task commands
18#[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    /// Submit evaluation request to prover AVS
28    #[command(name = "submit-evaluation-request")]
29    SubmitEvaluationRequest(SubmitEvaluationRequestCommand),
30}
31
32/// Submit evaluation request command
33#[derive(Debug, Parser)]
34pub struct SubmitEvaluationRequestCommand {
35    /// Path to task JSON file
36    #[arg(long)]
37    task_json: PathBuf,
38
39    #[arg(long, env = "PRIVATE_KEY")]
40    private_key: Option<String>,
41
42    /// API key for authentication
43    #[arg(long, env = "API_KEY")]
44    api_key: Option<String>,
45}
46
47// Static counter for JSON-RPC request IDs
48static 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
63// Helper function to convert hex string to U256
64fn 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
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 to convert hex string to Bytes
137fn 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
143// Helper functions to get values from JSON
144fn 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
172// Convert raw JSON intent to TaskIntent
173fn json_intent_to_task_intent(intent: &serde_json::Value) -> eyre::Result<TaskIntent> {
174    // Normalize value and chainId first
175    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    // Get data as hex string (with or without 0x prefix)
195    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    // Get function signature as hex string (with or without 0x prefix)
207    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    /// Execute the task command
231    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                // Get API key from:
239                // 1. Command line flag --api-key (highest priority)
240                // 2. API_KEY environment variable (from .env file)
241                // 3. Config file user_secret_key (from cli-login) (lowest priority)
242                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                // Convert raw JSON to TaskIntent (with normalization)
256                let task_intent = json_intent_to_task_intent(intent_json)?;
257
258                // Convert TaskIntent to ITaskIntent
259                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                // Get policy client
269                let policy_client = get_address(
270                    task.get("policyClient")
271                        .ok_or_else(|| eyre::eyre!("Missing policyClient"))?,
272                )?;
273
274                // Get optional fields
275                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                // Build ICreateTaskAndWaitRequest
285                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                // Sign using the existing RPC type method
296                let signed_request = CreateTaskAndWaitRequest::signed_from(&i_request, &private_key);
297
298                // Serialize to JSON for JSON-RPC request
299                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        // Reset counter by creating new instances (note: this is a static, so we can't truly reset)
330        // Just verify it returns incrementing values
331        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        // Empty string should fail or return 0
380        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        // Note: normalize_to_u256 only handles hex strings (via hex_to_u256), not decimal strings
424        // A decimal string like "100" will be interpreted as hex (which is 256 in decimal)
425        let value = serde_json::json!("100");
426        let result = normalize_to_u256(&value).unwrap();
427        // "100" as hex = 0x100 = 256 in decimal
428        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"); // 11155111 in hex
457    }
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"); // 100 in hex
469        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"); // Should add 0x prefix
630    }
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}