1
use std::fs::File;
2
use std::path::PathBuf;
3
use std::process::Command;
4
use std::str::FromStr;
5

            
6
use anyhow::{Context, Result};
7
use bonsol_prover::input_resolver::{ProgramInput, ResolvedInput};
8
use bonsol_sdk::instructions::CallbackConfig;
9
use bonsol_sdk::{InputT, InputType, ProgramInputType};
10
use clap::Args;
11
use rand::distributions::Alphanumeric;
12
use rand::Rng;
13
use serde::{Deserialize, Serialize};
14
use solana_rpc_client::nonblocking::rpc_client;
15
use solana_sdk::instruction::AccountMeta;
16
use solana_sdk::pubkey::Pubkey;
17

            
18
use crate::error::{BonsolCliError, ParseConfigError};
19

            
20
pub(crate) const MANIFEST_JSON: &str = "manifest.json";
21
pub(crate) const CARGO_COMMAND: &str = "cargo";
22
pub(crate) const CARGO_TOML: &str = "Cargo.toml";
23
pub(crate) const TARGET_DIR: &str = "target";
24
pub(crate) const CARGO_RISCZERO_VERSION: &str = "1.2.1";
25

            
26
pub fn cargo_has_plugin(plugin_name: &str) -> bool {
27
    Command::new("cargo")
28
        .args(["--list"])
29
        .output()
30
        .map(|output| {
31
            String::from_utf8_lossy(&output.stdout)
32
                .lines()
33
                .any(|line| line.trim().starts_with(plugin_name))
34
        })
35
        .unwrap_or(false)
36
}
37

            
38
pub fn has_executable(executable: &str) -> bool {
39
    Command::new("which")
40
        .arg(executable)
41
        .output()
42
        .map(|output| output.status.success())
43
        .unwrap_or(false)
44
}
45

            
46
#[derive(Debug, Deserialize, Serialize)]
47
#[serde(rename_all = "camelCase")]
48
pub struct ZkProgramManifest {
49
    pub name: String,
50
    pub binary_path: String,
51
    pub image_id: String,
52
    pub input_order: Vec<String>,
53
    pub signature: String,
54
    pub size: u64,
55
}
56

            
57
#[derive(Debug, Deserialize, Serialize, Clone, Args)]
58
#[serde(rename_all = "camelCase")]
59
pub struct CliInput {
60
    pub input_type: String,
61
    pub data: String, // hex encoded if binary with hex: prefix
62
}
63

            
64
#[derive(Debug, Clone)]
65
pub struct CliInputType(InputType);
66
impl ToString for CliInputType {
67
    fn to_string(&self) -> String {
68
        match self.0 {
69
            InputType::PublicData => "PublicData".to_string(),
70
            InputType::PublicAccountData => "PublicAccountData".to_string(),
71
            InputType::PublicUrl => "PublicUrl".to_string(),
72
            InputType::Private => "Private".to_string(),
73
            InputType::PublicProof => "PublicProof".to_string(),
74
            InputType::PrivateLocal => "PrivateUrl".to_string(),
75
            _ => "InvalidInputType".to_string(),
76
        }
77
    }
78
}
79

            
80
impl FromStr for CliInputType {
81
    type Err = anyhow::Error;
82

            
83
14
    fn from_str(s: &str) -> Result<Self, Self::Err> {
84
14
        match s {
85
14
            "PublicData" => Ok(CliInputType(InputType::PublicData)),
86
2
            "PublicAccountData" => Ok(CliInputType(InputType::PublicAccountData)),
87
2
            "PublicUrl" => Ok(CliInputType(InputType::PublicUrl)),
88
            "Private" => Ok(CliInputType(InputType::Private)),
89
            "PublicProof" => Ok(CliInputType(InputType::PublicProof)),
90
            "PrivateUrl" => Ok(CliInputType(InputType::PrivateLocal)),
91
            _ => Err(anyhow::anyhow!("Invalid input type")),
92
        }
93
14
    }
94
}
95

            
96
#[derive(Debug, Clone, Serialize, Deserialize)]
97
#[serde(rename_all = "camelCase")]
98
pub struct ExecutionRequestFile {
99
    pub image_id: Option<String>,
100
    pub execution_config: CliExecutionConfig,
101
    pub execution_id: Option<String>,
102
    pub tip: Option<u64>,
103
    pub expiry: Option<u64>,
104
    pub inputs: Option<Vec<CliInput>>,
105
    pub callback_config: Option<CliCallbackConfig>,
106
}
107

            
108
#[derive(Debug, Clone, Serialize, Deserialize)]
109
#[serde(rename_all = "camelCase")]
110
pub struct CliExecutionConfig {
111
    pub verify_input_hash: Option<bool>,
112
    pub input_hash: Option<String>,
113
    pub forward_output: Option<bool>,
114
}
115

            
116
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
117
#[serde(rename_all = "camelCase")]
118
pub struct CliCallbackConfig {
119
    #[serde(with = "bonsol_sdk::instructions::serde_helpers::optpubkey")]
120
    pub program_id: Option<Pubkey>,
121
    pub instruction_prefix: Option<Vec<u8>>,
122
    pub extra_accounts: Option<Vec<CliAccountMeta>>,
123
}
124

            
125
impl From<CliCallbackConfig> for CallbackConfig {
126
    fn from(val: CliCallbackConfig) -> Self {
127
        CallbackConfig {
128
            program_id: val.program_id.unwrap_or_default(),
129
            instruction_prefix: val.instruction_prefix.unwrap_or_default(),
130
            extra_accounts: val
131
                .extra_accounts
132
                .map(|v| v.into_iter().map(|a| a.into()).collect())
133
                .unwrap_or_default(),
134
        }
135
    }
136
}
137

            
138
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
139
#[serde(rename_all = "camelCase")]
140
pub struct CliAccountMeta {
141
    #[serde(default, with = "bonsol_sdk::instructions::serde_helpers::pubkey")]
142
    pub pubkey: Pubkey,
143
    pub is_signer: bool,
144
    pub is_writable: bool,
145
}
146

            
147
impl From<CliAccountMeta> for AccountMeta {
148
    fn from(val: CliAccountMeta) -> Self {
149
        AccountMeta {
150
            pubkey: val.pubkey,
151
            is_signer: val.is_signer,
152
            is_writable: val.is_writable,
153
        }
154
    }
155
}
156

            
157
#[derive(Debug, Clone, Serialize, Deserialize)]
158
#[serde(rename_all = "camelCase")]
159
pub struct InputFile {
160
    pub inputs: Vec<CliInput>,
161
}
162

            
163
/// Attempt to load the RPC URL and keypair file from a solana `config.yaml`.
164
pub(crate) fn try_load_from_config(config: Option<String>) -> anyhow::Result<(String, String)> {
165
    let whoami = String::from_utf8_lossy(&std::process::Command::new("whoami").output()?.stdout)
166
        .trim_end()
167
        .to_string();
168
    let default_config_path = solana_cli_config::CONFIG_FILE.as_ref();
169

            
170
    let config_file = config.as_ref().map_or_else(
171
        || -> anyhow::Result<&String> {
172
            let inner_err = ParseConfigError::DefaultConfigNotFound {
173
                whoami: whoami.clone(),
174
            };
175
            let context = inner_err.context(None);
176

            
177
            // If no config is given, try to find it at the default location.
178
            default_config_path
179
                .and_then(|s| PathBuf::from_str(s).is_ok_and(|p| p.exists()).then_some(s))
180
                .ok_or(BonsolCliError::ParseConfigError(inner_err))
181
                .context(context)
182
        },
183
        |config| -> anyhow::Result<&String> {
184
            // Here we throw an error if the user provided a path to a config that does not exist.
185
            // Instead of using the default location, it's better to show the user the path they
186
            // expected to use was not valid.
187
            if !PathBuf::from_str(config)?.exists() {
188
                let inner_err = ParseConfigError::ConfigNotFound {
189
                    path: config.into(),
190
                };
191
                let context = inner_err.context(None);
192
                let err: anyhow::Error = BonsolCliError::ParseConfigError(inner_err).into();
193
                return Err(err.context(context));
194
            }
195
            Ok(config)
196
        },
197
    )?;
198
    let config = {
199
        let mut inner_err = ParseConfigError::Uninitialized;
200

            
201
        let mut maybe_config = solana_cli_config::Config::load(config_file).map_err(|err| {
202
            let err = ParseConfigError::FailedToLoad {
203
                path: config.unwrap_or(default_config_path.cloned().unwrap()),
204
                err: format!("{err:?}"),
205
            };
206
            inner_err = err.clone();
207
            BonsolCliError::ParseConfigError(err).into()
208
        });
209
        if maybe_config.is_err() {
210
            maybe_config = maybe_config.context(inner_err.context(Some(whoami)));
211
        }
212
        maybe_config
213
    }?;
214
    Ok((config.json_rpc_url, config.keypair_path))
215
}
216

            
217
pub(crate) fn load_solana_config(
218
    config: Option<String>,
219
    rpc_url: Option<String>,
220
    keypair: Option<String>,
221
) -> anyhow::Result<(String, solana_sdk::signer::keypair::Keypair)> {
222
    let (rpc_url, keypair_file) = match rpc_url.zip(keypair) {
223
        Some(config) => config,
224
        None => try_load_from_config(config)?,
225
    };
226
    Ok((
227
        rpc_url,
228
        solana_sdk::signature::read_keypair_file(std::path::Path::new(&keypair_file)).map_err(
229
            |err| BonsolCliError::FailedToReadKeypair {
230
                file: keypair_file,
231
                err: format!("{err:?}"),
232
            },
233
        )?,
234
    ))
235
}
236

            
237
pub async fn sol_check(rpc_client: String, pubkey: Pubkey) -> bool {
238
    let rpc_client = rpc_client::RpcClient::new(rpc_client);
239
    if let Ok(account) = rpc_client.get_account(&pubkey).await {
240
        return account.lamports > 0;
241
    }
242
    false
243
}
244

            
245
pub fn execute_get_inputs(
246
    inputs_file: Option<String>,
247
    stdin: Option<String>,
248
) -> Result<Vec<CliInput>> {
249
    if let Some(std) = stdin {
250
        let parsed = serde_json::from_str::<InputFile>(&std)
251
            .map_err(|e| anyhow::anyhow!("Error parsing stdin: {:?}", e))?;
252
        return Ok(parsed.inputs);
253
    }
254

            
255
    if let Some(istr) = inputs_file {
256
        let ifile = File::open(istr)?;
257
        let parsed: InputFile = serde_json::from_reader(&ifile)
258
            .map_err(|e| anyhow::anyhow!("Error parsing inputs file: {:?}", e))?;
259
        return Ok(parsed.inputs);
260
    }
261

            
262
    Err(anyhow::anyhow!("No inputs provided"))
263
}
264

            
265
pub fn proof_get_inputs(
266
    inputs_file: Option<String>,
267
    stdin: Option<String>,
268
) -> Result<Vec<ProgramInput>> {
269
    if let Some(std) = stdin {
270
        return proof_parse_stdin(&std);
271
    }
272
    if let Some(istr) = inputs_file {
273
        return proof_parse_input_file(&istr);
274
    }
275
    Err(anyhow::anyhow!("No inputs provided"))
276
}
277

            
278
12
pub fn execute_transform_cli_inputs(inputs: Vec<CliInput>) -> Result<Vec<InputT>> {
279
12
    let mut res = vec![];
280
14
    for input in inputs.into_iter() {
281
14
        let input_type = CliInputType::from_str(&input.input_type)?.0;
282
14
        match input_type {
283
            InputType::PublicData => {
284
12
                if input.data.starts_with("0x") {
285
3
                    let (is_valid, data) = is_valid_hex(&input.data[2..]);
286
3
                    if is_valid {
287
2
                        res.push(InputT::public(data));
288
2
                    } else {
289
1
                        return Err(anyhow::anyhow!(
290
1
                            "Invalid hex string for PublicData: {}",
291
1
                            input.data
292
1
                        ));
293
                    }
294
9
                } else if let Some(n) = is_valid_number(&input.data) {
295
7
                    let data = n.into_bytes();
296
7
                    res.push(InputT::public(data));
297
7
                } else {
298
2
                    return Err(anyhow::anyhow!(
299
2
                        "PublicData input \'{}\' is not a valid number (i64, u64, f64) or 0x-prefixed hex string",
300
2
                        input.data
301
2
                    ));
302
                }
303
            }
304
2
            _ => {
305
2
                res.push(InputT::new(input_type, Some(input.data.into_bytes())));
306
2
            }
307
        }
308
    }
309
9
    Ok(res)
310
12
}
311

            
312
4
fn is_valid_hex(s: &str) -> (bool, Vec<u8>) {
313
4
    if s.len() % 2 != 0 {
314
1
        return (false, vec![]);
315
3
    }
316
68
    let is_hex_char = |c: char| c.is_ascii_hexdigit();
317
3
    if !s.chars().all(is_hex_char) {
318
        return (false, vec![]);
319
3
    }
320
3
    let out = hex::decode(s);
321
3
    (out.is_ok(), out.unwrap_or_default())
322
4
}
323

            
324
#[derive(Debug, PartialEq)]
325
pub enum NumberType {
326
    Float(f64),
327
    Unsigned(u64),
328
    Integer(i64),
329
    // TODO: add BigInt
330
}
331

            
332
impl NumberType {
333
7
    fn into_bytes(&self) -> Vec<u8> {
334
7
        match self {
335
1
            NumberType::Float(f) => f.to_le_bytes().to_vec(),
336
3
            NumberType::Unsigned(u) => u.to_le_bytes().to_vec(),
337
3
            NumberType::Integer(i) => i.to_le_bytes().to_vec(),
338
        }
339
7
    }
340
}
341

            
342
14
fn is_valid_number(s: &str) -> Option<NumberType> {
343
14
    if let Ok(num) = s.parse::<u64>() {
344
4
        return Some(NumberType::Unsigned(num));
345
10
    }
346
10
    if let Ok(num) = s.parse::<i64>() {
347
4
        return Some(NumberType::Integer(num));
348
6
    }
349
6
    if let Ok(num) = s.parse::<f64>() {
350
2
        return Some(NumberType::Float(num));
351
4
    }
352
4
    None
353
14
}
354

            
355
6
fn proof_parse_entry(index: u8, s: &str, input_type_str: &str) -> Result<ProgramInput> {
356
6
    let program_input_type = match input_type_str.to_lowercase().as_str() {
357
6
        "public" | "publicdata" => ProgramInputType::Public,
358
        "private" | "privatedata" => ProgramInputType::Private,
359
        _ => ProgramInputType::Public,
360
    };
361

            
362
6
    if let Ok(num) = s.parse::<i64>() {
363
2
        return Ok(ProgramInput::Resolved(ResolvedInput {
364
2
            index,
365
2
            data: num.to_le_bytes().to_vec(),
366
2
            input_type: program_input_type,
367
2
        }));
368
4
    }
369
4
    if let Ok(num) = s.parse::<f64>() {
370
1
        return Ok(ProgramInput::Resolved(ResolvedInput {
371
1
            index,
372
1
            data: num.to_le_bytes().to_vec(),
373
1
            input_type: program_input_type,
374
1
        }));
375
3
    }
376
3
    if let Ok(num) = s.parse::<u64>() {
377
        return Ok(ProgramInput::Resolved(ResolvedInput {
378
            index,
379
            data: num.to_le_bytes().to_vec(),
380
            input_type: program_input_type,
381
        }));
382
3
    }
383
3
    let has_hex_prefix = s.starts_with("0x");
384
3
    if has_hex_prefix {
385
1
        let (is_valid, data) = is_valid_hex(&s[2..]);
386
1
        if is_valid {
387
1
            return Ok(ProgramInput::Resolved(ResolvedInput {
388
1
                index,
389
1
                data,
390
1
                input_type: program_input_type,
391
1
            }));
392
        } else {
393
            return Err(anyhow::anyhow!("Invalid hex data"));
394
        }
395
2
    }
396
2
    return Ok(ProgramInput::Resolved(ResolvedInput {
397
2
        index,
398
2
        data: s.as_bytes().to_vec(),
399
2
        input_type: program_input_type,
400
2
    }));
401
6
}
402

            
403
fn proof_parse_input_file(input_file_path: &str) -> Result<Vec<ProgramInput>> {
404
    println!(
405
        "[BONSOL_DEBUG] proof_parse_input_file: Attempting to read input file: '{}'",
406
        input_file_path
407
    );
408
    let file_content_str = match std::fs::read_to_string(input_file_path) {
409
        Ok(content) => {
410
            println!("[BONSOL_DEBUG] proof_parse_input_file: Successfully read file. Content length: {}. First 100 chars: {:?}", content.len(), content.chars().take(100).collect::<String>());
411
            // To see all bytes, which can reveal BOMs or other non-printable chars:
412
            // println!("[BONSOL_DEBUG] proof_parse_input_file: File content as bytes: {:?}", content.as_bytes());
413
            content
414
        }
415
        Err(e) => {
416
            println!(
417
                "[BONSOL_DEBUG] proof_parse_input_file: Failed to read file: {:?}",
418
                e
419
            );
420
            return Err(e).with_context(|| {
421
                format!("Failed to read input file at path: {}", input_file_path)
422
            });
423
        }
424
    };
425

            
426
    println!("[BONSOL_DEBUG] proof_parse_input_file: Attempting to deserialize JSON from file content...");
427
    match serde_json::from_str::<InputFile>(&file_content_str) {
428
        Ok(ifile) => {
429
            println!("[BONSOL_DEBUG] proof_parse_input_file: Successfully deserialized JSON into InputFile struct.");
430
            let len = ifile.inputs.len();
431
            println!(
432
                "[BONSOL_DEBUG] proof_parse_input_file: Number of input entries in JSON: {}",
433
                len
434
            );
435

            
436
            let mut parsed_inputs_accumulator: Vec<ProgramInput> = Vec::new();
437
            for (index, cli_input_item) in ifile.inputs.into_iter().enumerate() {
438
                println!("[BONSOL_DEBUG] proof_parse_input_file: Processing entry {}: inputType='{}', data='{}'", index, cli_input_item.input_type, cli_input_item.data);
439
                match proof_parse_entry(
440
                    index as u8,
441
                    &cli_input_item.data,
442
                    &cli_input_item.input_type,
443
                ) {
444
                    Ok(program_input) => {
445
                        println!(
446
                            "[BONSOL_DEBUG] proof_parse_input_file: Successfully parsed entry {}",
447
                            index
448
                        );
449
                        parsed_inputs_accumulator.push(program_input);
450
                    }
451
                    Err(e) => {
452
                        println!(
453
                            "[BONSOL_DEBUG] proof_parse_input_file: Failed to parse entry {}: {:?}",
454
                            index, e
455
                        );
456
                        // Return a more specific error including which entry failed if possible
457
                        return Err(anyhow::anyhow!(
458
                            "Invalid input file (entry {} failed to parse: {})",
459
                            index,
460
                            e
461
                        ));
462
                    }
463
                }
464
            }
465

            
466
            // This check is essentially done by the loop returning an error on first failure.
467
            // if parsed_inputs_accumulator.len() != len {
468
            //     println!("[BONSOL_DEBUG] proof_parse_input_file: Mismatch in parsed entries count. Expected: {}, Got: {}", len, parsed_inputs_accumulator.len());
469
            //     return Err(anyhow::anyhow!("Invalid input file (an entry failed to parse - count mismatch)"));
470
            // }
471
            println!("[BONSOL_DEBUG] proof_parse_input_file: All entries processed successfully.");
472
            return Ok(parsed_inputs_accumulator);
473
        }
474
        Err(e) => {
475
            println!(
476
                "[BONSOL_DEBUG] proof_parse_input_file: JSON deserialization failed: {:?}",
477
                e
478
            );
479
            let snippet_len = std::cmp::min(file_content_str.len(), 200); // Show a snippet of the problematic string
480
            println!("[BONSOL_DEBUG] proof_parse_input_file: JSON parsing failed on (first {} chars): <{}>", snippet_len, &file_content_str[..snippet_len]);
481
            // Return a more specific error including the serde error
482
            return Err(anyhow::anyhow!(
483
                "Invalid input file (JSON deserialization failed: {})",
484
                e
485
            ));
486
        }
487
    }
488
}
489

            
490
1
fn proof_parse_stdin(input: &str) -> Result<Vec<ProgramInput>> {
491
1
    let mut entries = Vec::new();
492
1
    let mut current_entry = String::new();
493
1
    let mut in_quotes = false;
494
1
    let mut in_brackets = 0;
495
93
    for c in input.chars() {
496
2
        match c {
497
2
            '"' if !in_quotes => in_quotes = true,
498
2
            '"' if in_quotes => in_quotes = false,
499
1
            '{' | '[' if !in_quotes => in_brackets += 1,
500
1
            '}' | ']' if !in_quotes => in_brackets -= 1,
501
5
            ' ' if !in_quotes && in_brackets == 0 && !current_entry.is_empty() => {
502
5
                let index = entries.len() as u8;
503
5
                entries.push(proof_parse_entry(index, &current_entry, "PublicData")?);
504
5
                current_entry.clear();
505
5
                continue;
506
            }
507
82
            _ => {}
508
        }
509
88
        current_entry.push(c);
510
    }
511
1
    if !current_entry.is_empty() {
512
1
        entries.push(proof_parse_entry(
513
1
            entries.len() as u8,
514
1
            &current_entry,
515
1
            "PublicData",
516
1
        )?);
517
    }
518
1
    Ok(entries)
519
1
}
520

            
521
pub fn rand_id(chars: usize) -> String {
522
    let mut rng = rand::thread_rng();
523
    (&mut rng)
524
        .sample_iter(Alphanumeric)
525
        .take(chars)
526
        .map(char::from)
527
        .collect()
528
}
529

            
530
#[cfg(test)]
531
mod test {
532
    use super::*;
533
    use hex;
534

            
535
    #[test]
536
1
    fn test_proof_parse_stdin() {
537
1
        let inputs = r#"1234567890abcdef 0x313233343536373839313061626364656667 2.1 2000 -2000 {"attestation":"test"}"#;
538
1
        let inputs_parsed = proof_parse_stdin(inputs).unwrap();
539
1

            
540
1
        let expected_inputs = vec![
541
1
            ProgramInput::Resolved(ResolvedInput {
542
1
                index: 0,
543
1
                data: "1234567890abcdef".as_bytes().to_vec(),
544
1
                input_type: ProgramInputType::Public,
545
1
            }),
546
1
            ProgramInput::Resolved(ResolvedInput {
547
1
                index: 1,
548
1
                data: hex::decode("313233343536373839313061626364656667").unwrap(),
549
1
                input_type: ProgramInputType::Public,
550
1
            }),
551
1
            ProgramInput::Resolved(ResolvedInput {
552
1
                index: 2,
553
1
                data: 2.1f64.to_le_bytes().to_vec(),
554
1
                input_type: ProgramInputType::Public,
555
1
            }),
556
1
            ProgramInput::Resolved(ResolvedInput {
557
1
                index: 3,
558
1
                data: 2000u64.to_le_bytes().to_vec(),
559
1
                input_type: ProgramInputType::Public,
560
1
            }),
561
1
            ProgramInput::Resolved(ResolvedInput {
562
1
                index: 4,
563
1
                data: (-2000i64).to_le_bytes().to_vec(),
564
1
                input_type: ProgramInputType::Public,
565
1
            }),
566
1
            ProgramInput::Resolved(ResolvedInput {
567
1
                index: 5,
568
1
                data: "{\"attestation\":\"test\"}".as_bytes().to_vec(),
569
1
                input_type: ProgramInputType::Public,
570
1
            }),
571
1
        ];
572
1
        assert_eq!(inputs_parsed, expected_inputs);
573
1
    }
574

            
575
    #[test]
576
1
    fn test_is_valid_number() {
577
1
        let num = is_valid_number("1234567890abcdef");
578
1
        assert!(num.is_none());
579
1
        let num = is_valid_number("1234567890abcdefg");
580
1
        assert!(num.is_none());
581
1
        let num = is_valid_number("2.1");
582
1
        assert!(num.is_some());
583
1
        assert_eq!(num.unwrap(), NumberType::Float(2.1));
584
1
        let num = is_valid_number("2000");
585
1
        assert!(num.is_some());
586
1
        assert_eq!(num.unwrap(), NumberType::Unsigned(2000));
587
1
        let num = is_valid_number("-2000");
588
1
        assert!(num.is_some());
589
1
        assert_eq!(num.unwrap(), NumberType::Integer(-2000));
590
1
    }
591

            
592
    #[test]
593
1
    fn test_execute_transform_cli_inputs() {
594
1
        // Case 1: Invalid PublicData (non-numeric, non-hex string) - Should now be an error
595
1
        let invalid_public_data_str = CliInput {
596
1
            input_type: "PublicData".to_string(),
597
1
            data: "1234567890abcdef".to_string(), // This was the old first test case
598
1
        };
599
1
        assert!(execute_transform_cli_inputs(vec![invalid_public_data_str]).is_err());
600

            
601
        // Case 2: Valid hex PublicData
602
1
        let hex_input = CliInput {
603
1
            input_type: "PublicData".to_string(),
604
1
            data: "0x0102030405060708".to_string(), // Using a clear 8-byte hex for i64 tests later
605
1
        };
606
1
        let expected_hex_bytes = hex::decode("0102030405060708").unwrap();
607
1
        assert_eq!(
608
1
            execute_transform_cli_inputs(vec![hex_input]).unwrap(),
609
1
            vec![InputT::public(expected_hex_bytes)]
610
1
        );
611

            
612
        // Case 3: PublicData with f64 string
613
1
        let float_input = CliInput {
614
1
            input_type: "PublicData".to_string(),
615
1
            data: "2.1".to_string(),
616
1
        };
617
1
        assert_eq!(
618
1
            execute_transform_cli_inputs(vec![float_input]).unwrap(),
619
1
            vec![InputT::public(2.1f64.to_le_bytes().to_vec())]
620
1
        );
621

            
622
        // Case 4: PublicData with u64 string
623
1
        let u64_input = CliInput {
624
1
            input_type: "PublicData".to_string(),
625
1
            data: "2000".to_string(),
626
1
        };
627
1
        assert_eq!(
628
1
            execute_transform_cli_inputs(vec![u64_input]).unwrap(),
629
1
            vec![InputT::public(2000u64.to_le_bytes().to_vec())]
630
1
        );
631

            
632
        // Case 5: PublicData with i64 string (negative)
633
1
        let i64_neg_input = CliInput {
634
1
            input_type: "PublicData".to_string(),
635
1
            data: "-2000".to_string(),
636
1
        };
637
1
        assert_eq!(
638
1
            execute_transform_cli_inputs(vec![i64_neg_input]).unwrap(),
639
1
            vec![InputT::public((-2000i64).to_le_bytes().to_vec())]
640
1
        );
641

            
642
        // Case 6: New - PublicData with positive i64 string
643
1
        let i64_pos_input = CliInput {
644
1
            input_type: "PublicData".to_string(),
645
1
            data: "123".to_string(),
646
1
        };
647
1
        assert_eq!(
648
1
            execute_transform_cli_inputs(vec![i64_pos_input]).unwrap(),
649
1
            vec![InputT::public(123i64.to_le_bytes().to_vec())]
650
1
        );
651

            
652
        // Case 7: New - PublicData with another negative i64 string
653
1
        let i64_neg2_input = CliInput {
654
1
            input_type: "PublicData".to_string(),
655
1
            data: "-456".to_string(),
656
1
        };
657
1
        assert_eq!(
658
1
            execute_transform_cli_inputs(vec![i64_neg2_input]).unwrap(),
659
1
            vec![InputT::public((-456i64).to_le_bytes().to_vec())]
660
1
        );
661

            
662
        // Case 8: New - PublicData with invalid string (non-numeric, non-hex) - explicit test
663
1
        let invalid_str_input = CliInput {
664
1
            input_type: "PublicData".to_string(),
665
1
            data: "hello".to_string(),
666
1
        };
667
1
        assert!(execute_transform_cli_inputs(vec![invalid_str_input]).is_err());
668

            
669
        // Case 9: New - PublicData with invalid hex string
670
1
        let invalid_hex_input = CliInput {
671
1
            input_type: "PublicData".to_string(),
672
1
            data: "0xNOTAHEX".to_string(),
673
1
        };
674
1
        assert!(execute_transform_cli_inputs(vec![invalid_hex_input]).is_err());
675

            
676
        // Case 10: New - Non-PublicData type (e.g., PublicUrl) - should pass through string as bytes
677
1
        let public_url_input = CliInput {
678
1
            input_type: "PublicUrl".to_string(), // Assuming PublicUrl is a valid CliInputType string
679
1
            data: "mytesturl".to_string(),
680
1
        };
681
1
        // Need to ensure InputT::new for PublicUrl results in the correct InputType enum variant
682
1
        // For this, we might need to know the mapping or have CliInputType also in scope for direct construction.
683
1
        // Assuming CliInputType::from_str("PublicUrl").unwrap().0 gives InputType::PublicUrl
684
1
        assert_eq!(
685
1
            execute_transform_cli_inputs(vec![public_url_input]).unwrap(),
686
1
            vec![InputT::new(
687
1
                InputType::PublicUrl,
688
1
                Some("mytesturl".as_bytes().to_vec())
689
1
            )]
690
1
        );
691

            
692
        // Test with multiple inputs
693
1
        let inputs_multiple = vec![
694
1
            CliInput {
695
1
                input_type: "PublicData".to_string(),
696
1
                data: "3".to_string(),
697
1
            },
698
1
            CliInput {
699
1
                input_type: "PublicData".to_string(),
700
1
                data: "0x0a00000000000000".to_string(),
701
1
            }, // 10 as hex i64 LE
702
1
            CliInput {
703
1
                input_type: "PublicUrl".to_string(),
704
1
                data: "test.com".to_string(),
705
1
            },
706
1
            CliInput {
707
1
                input_type: "PublicData".to_string(),
708
1
                data: "-5".to_string(),
709
1
            },
710
1
        ];
711
1
        let parsed_multiple = execute_transform_cli_inputs(inputs_multiple).unwrap();
712
1
        assert_eq!(
713
1
            parsed_multiple,
714
1
            vec![
715
1
                InputT::public(3i64.to_le_bytes().to_vec()),
716
1
                InputT::public(hex::decode("0a00000000000000").unwrap()),
717
1
                InputT::new(InputType::PublicUrl, Some("test.com".as_bytes().to_vec())),
718
1
                InputT::public((-5i64).to_le_bytes().to_vec()),
719
1
            ]
720
1
        );
721

            
722
        // Test with an empty input vector
723
1
        let empty_inputs: Vec<CliInput> = Vec::new();
724
1
        assert_eq!(
725
1
            execute_transform_cli_inputs(empty_inputs).unwrap(),
726
1
            Vec::new()
727
1
        );
728
1
    }
729
}