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