Skip to main content

casper_client/cli/
parse.rs

1//! This module contains structs and helpers which are used by multiple subcommands related to
2//! creating deploys.
3
4use std::fs;
5#[cfg(feature = "std-fs-io")]
6use std::path::Path;
7use std::str::FromStr;
8
9use rand::Rng;
10
11#[cfg(feature = "std-fs-io")]
12use casper_types::SecretKey;
13use casper_types::{
14    account::AccountHash, bytesrepr::Bytes, crypto, AsymmetricType, BlockHash, DeployHash, Digest,
15    EntityAddr, ExecutableDeployItem, HashAddr, Key, NamedArg, PricingMode, PublicKey, RuntimeArgs,
16    TimeDiff, Timestamp, TransactionArgs, TransactionHash, TransactionV1Hash, TransferTarget,
17    UIntParseError, URef, U512,
18};
19
20use super::{simple_args, CliError, PaymentStrParams, SessionStrParams};
21#[cfg(feature = "std-fs-io")]
22use crate::OutputKind;
23use crate::{
24    rpcs::EraIdentifier, AccountIdentifier, BlockIdentifier, EntityIdentifier,
25    GlobalStateIdentifier, JsonRpcId, PurseIdentifier, Verbosity,
26};
27
28pub(super) fn rpc_id(maybe_rpc_id: &str) -> JsonRpcId {
29    if maybe_rpc_id.is_empty() {
30        JsonRpcId::from(rand::thread_rng().gen::<i64>())
31    } else if let Ok(i64_id) = maybe_rpc_id.parse::<i64>() {
32        JsonRpcId::from(i64_id)
33    } else {
34        JsonRpcId::from(maybe_rpc_id.to_string())
35    }
36}
37
38pub(super) fn verbosity(verbosity_level: u64) -> Verbosity {
39    match verbosity_level {
40        0 => Verbosity::Low,
41        1 => Verbosity::Medium,
42        _ => Verbosity::High,
43    }
44}
45
46#[cfg(feature = "std-fs-io")]
47pub(super) fn output_kind(maybe_output_path: &str, force: bool) -> OutputKind {
48    if maybe_output_path.is_empty() {
49        OutputKind::Stdout
50    } else {
51        OutputKind::file(Path::new(maybe_output_path), force)
52    }
53}
54
55#[cfg(feature = "std-fs-io")]
56pub(super) fn secret_key_from_file<P: AsRef<Path>>(
57    secret_key_path: P,
58) -> Result<SecretKey, CliError> {
59    SecretKey::from_file(secret_key_path).map_err(|error| {
60        CliError::Core(crate::Error::CryptoError {
61            context: "secret key",
62            error,
63        })
64    })
65}
66
67pub(super) fn timestamp(value: &str) -> Result<Timestamp, CliError> {
68    #[cfg(any(feature = "std-fs-io", test))]
69    let timestamp = Timestamp::now();
70    #[cfg(not(any(feature = "std-fs-io", test)))]
71    let timestamp = Timestamp::zero();
72    if value.is_empty() {
73        return Ok(timestamp);
74    }
75    Timestamp::from_str(value).map_err(|error| CliError::FailedToParseTimestamp {
76        context: "timestamp",
77        error,
78    })
79}
80
81pub(super) fn ttl(value: &str) -> Result<TimeDiff, CliError> {
82    TimeDiff::from_str(value).map_err(|error| CliError::FailedToParseTimeDiff {
83        context: "ttl",
84        error,
85    })
86}
87
88pub(super) fn session_account(value: &str) -> Result<Option<PublicKey>, CliError> {
89    if value.is_empty() {
90        return Ok(None);
91    }
92
93    let public_key = PublicKey::from_hex(value).map_err(|error| crate::Error::CryptoError {
94        context: "session account",
95        error: crypto::ErrorExt::from(error),
96    })?;
97    Ok(Some(public_key))
98}
99
100/// Handles providing the arg for and retrieval of simple session and payment args.
101pub(crate) mod arg_simple {
102    use super::*;
103
104    pub(crate) mod session {
105        use super::*;
106
107        pub fn parse(values: &[&str]) -> Result<Option<RuntimeArgs>, CliError> {
108            Ok(if values.is_empty() {
109                None
110            } else {
111                Some(get(values)?)
112            })
113        }
114    }
115
116    pub(crate) mod payment {
117        use super::*;
118
119        pub fn parse(values: &[&str]) -> Result<Option<RuntimeArgs>, CliError> {
120            Ok(if values.is_empty() {
121                None
122            } else {
123                Some(get(values)?)
124            })
125        }
126    }
127
128    fn get(values: &[&str]) -> Result<RuntimeArgs, CliError> {
129        let mut runtime_args = RuntimeArgs::new();
130        for arg in values {
131            simple_args::insert_arg(arg, &mut runtime_args)?;
132        }
133        Ok(runtime_args)
134    }
135}
136
137pub(crate) mod args_json {
138    use super::*;
139    use crate::cli::JsonArg;
140
141    pub mod session {
142        use super::*;
143
144        pub fn parse(json_str: &str) -> Result<Option<RuntimeArgs>, CliError> {
145            get(json_str)
146        }
147    }
148
149    pub mod payment {
150        use super::*;
151
152        pub fn parse(json_str: &str) -> Result<Option<RuntimeArgs>, CliError> {
153            get(json_str)
154        }
155    }
156
157    fn get(json_str: &str) -> Result<Option<RuntimeArgs>, CliError> {
158        if json_str.is_empty() {
159            return Ok(None);
160        }
161        let json_args: Vec<JsonArg> = serde_json::from_str(json_str)?;
162        let mut named_args = Vec::with_capacity(json_args.len());
163        for json_arg in json_args {
164            named_args.push(NamedArg::try_from(json_arg)?);
165        }
166        Ok(Some(RuntimeArgs::from(named_args)))
167    }
168}
169
170const STANDARD_PAYMENT_ARG_NAME: &str = "amount";
171fn standard_payment(value: &str) -> Result<RuntimeArgs, CliError> {
172    if value.is_empty() {
173        return Err(CliError::InvalidCLValue(value.to_string()));
174    }
175    let arg = U512::from_dec_str(value).map_err(|err| CliError::FailedToParseUint {
176        context: "amount",
177        error: UIntParseError::FromDecStr(err),
178    })?;
179    let mut runtime_args = RuntimeArgs::new();
180    runtime_args.insert(STANDARD_PAYMENT_ARG_NAME, arg)?;
181    Ok(runtime_args)
182}
183
184/// Checks if conflicting arguments are provided for parsing session information.
185///
186/// # Arguments
187///
188/// * `context` - A string indicating the context in which the arguments are checked.
189/// * `simple` - A vector of strings representing simple arguments.
190/// * `json` - A string representing JSON-formatted arguments.
191///
192/// # Returns
193///
194/// Returns a `Result` with an empty `Ok(())` variant if no conflicting arguments are found. If
195/// conflicting arguments are provided, an `Err` variant with a `CliError::ConflictingArguments` is
196/// returned.
197///
198/// # Errors
199///
200/// Returns an `Err` variant with a `CliError::ConflictingArguments` if conflicting arguments are
201/// provided.
202fn check_no_conflicting_arg_types(
203    context: &str,
204    simple: &[&str],
205    json: &str,
206) -> Result<(), CliError> {
207    let count = [!simple.is_empty(), !json.is_empty()]
208        .iter()
209        .filter(|&&x| x)
210        .count();
211
212    if count > 1 {
213        return Err(CliError::ConflictingArguments {
214            context: format!("{context} args conflict (simple json)",),
215            args: vec![simple.join(", "), json.to_owned()],
216        });
217    }
218    Ok(())
219}
220
221/// Constructs a `RuntimeArgs` instance from either simple or JSON representation.
222///
223/// # Arguments
224///
225/// * `simple`: An optional `RuntimeArgs` representing simple arguments.
226/// * `json`: An optional `RuntimeArgs` representing arguments in JSON format.
227///
228/// # Returns
229///
230/// A `RuntimeArgs` instance representing the merged arguments from `simple` and `json`.
231///
232/// # Examples
233///
234/// ```
235/// use casper_client::cli::parse::args_from_simple_or_json;
236/// use casper_types::RuntimeArgs;
237///
238/// let simple_args = RuntimeArgs::new(); // Simple arguments
239/// let json_args = RuntimeArgs::new();   // JSON arguments
240///
241/// let _result_args = args_from_simple_or_json(Some(simple_args), None, None);
242/// let _result_args = args_from_simple_or_json(None, Some(json_args), None);
243/// ```
244pub fn args_from_simple_or_json(
245    simple: Option<RuntimeArgs>,
246    json: Option<RuntimeArgs>,
247    chunked: Option<Vec<u8>>,
248) -> TransactionArgs {
249    // We can have exactly zero or one of the two as `Some`.
250    match chunked {
251        Some(chunked) => TransactionArgs::Bytesrepr(chunked.into()),
252        None => {
253            let named_args = match (simple, json) {
254                (Some(args), None) | (None, Some(args)) => args,
255                (None, None) => RuntimeArgs::new(),
256                _ => unreachable!("should not have more than one of simple, json args"),
257            };
258            TransactionArgs::Named(named_args)
259        }
260    }
261}
262
263/// Private macro for enforcing parameter validity.
264/// e.g. check_exactly_one_not_empty!(
265///   (field1) requires[another_field],
266///   (field2) requires[another_field, yet_another_field]
267///   (field3) requires[]
268/// )
269/// Returns an error if:
270/// - More than one parameter is non-empty.
271/// - Any parameter that is non-empty has requires[] requirements that are empty.
272macro_rules! check_exactly_one_not_empty {
273    ( context: $site:tt, $( ($x:expr) requires[$($y:expr),*] requires_empty[$($z:expr),*] ),+ $(,)? ) => {{
274
275        let field_is_empty_map = &[$(
276            (stringify!($x), $x.is_empty())
277        ),+];
278
279        let required_arguments = field_is_empty_map
280            .iter()
281            .filter(|(_, is_empty)| !*is_empty)
282            .map(|(field, _)| field.to_string())
283            .collect::<Vec<_>>();
284
285        if required_arguments.is_empty() {
286            let required_param_names = vec![$((stringify!($x))),+];
287            return Err(CliError::InvalidArgument {
288                context: $site,
289                error: format!("Missing a required arg - exactly one of the following must be provided: {:?}", required_param_names),
290            });
291        }
292        if required_arguments.len() == 1 {
293            let name = &required_arguments[0];
294            let field_requirements = &[$(
295                (
296                    stringify!($x),
297                    $x,
298                    vec![$((stringify!($y), $y)),*],
299                    vec![$((stringify!($z), $z)),*],
300                )
301            ),+];
302
303            // Check requires[] and requires_requires_empty[] fields
304            let (_, value, requirements, required_empty) = field_requirements
305                .iter()
306                .find(|(field, _, _, _)| *field == name).expect("should exist");
307            let required_arguments = requirements
308                .iter()
309                .filter(|(_, value)| !value.is_empty())
310                .collect::<Vec<_>>();
311
312            if requirements.len() != required_arguments.len() {
313                let required_param_names = requirements
314                    .iter()
315                    .map(|(requirement_name, _)| requirement_name)
316                    .collect::<Vec<_>>();
317                return Err(CliError::InvalidArgument {
318                    context: $site,
319                    error: format!("Field {} also requires following fields to be provided: {:?}", name, required_param_names),
320                });
321            }
322
323            let mut conflicting_fields = required_empty
324                .iter()
325                .filter(|(_, value)| !value.is_empty())
326                .map(|(field, value)| format!("{}={}", field, value)).collect::<Vec<_>>();
327
328            if !conflicting_fields.is_empty() {
329                conflicting_fields.push(format!("{}={}", name, value));
330                conflicting_fields.sort();
331                return Err(CliError::ConflictingArguments{
332                    context: $site.to_string(),
333                    args: conflicting_fields,
334                });
335            }
336        } else {
337            // Here we have more than one non-empty arg, so it is an error.  Collect all non-empty
338            // fields and their values into a string to populate the returned Error.
339            let mut non_empty_fields_with_values = [$((stringify!($x), $x)),+]
340                .iter()
341                .filter_map(|(field_name, field_value)| if !field_value.is_empty() {
342                    Some(format!("{}={}", field_name, field_value))
343                } else {
344                    None
345                })
346                .collect::<Vec<String>>();
347            non_empty_fields_with_values.sort();
348            return Err(CliError::ConflictingArguments {
349                context: $site.to_string(),
350                args: non_empty_fields_with_values,
351            });
352        }
353    }}
354}
355
356pub(super) fn session_executable_deploy_item(
357    params: SessionStrParams,
358) -> Result<ExecutableDeployItem, CliError> {
359    let SessionStrParams {
360        session_hash,
361        session_name,
362        session_package_hash,
363        session_package_name,
364        session_path,
365        session_bytes,
366        ref session_args_simple,
367        session_args_json,
368        session_version,
369        session_entry_point,
370        is_session_transfer: session_transfer,
371        session_chunked_args,
372    } = params;
373    // This is to make sure that we're using &str consistently in the macro call below.
374    let is_session_transfer = if session_transfer { "true" } else { "" };
375    // This is to make sure that we're using &str consistently in the macro call below.
376    let has_session_bytes = if session_bytes.is_empty() { "" } else { "true" };
377
378    check_exactly_one_not_empty!(
379        context: "parse_session_info",
380        (session_hash)
381            requires[session_entry_point] requires_empty[session_version],
382        (session_name)
383            requires[session_entry_point] requires_empty[session_version],
384        (session_package_hash)
385            requires[session_entry_point] requires_empty[],
386        (session_package_name)
387            requires[session_entry_point] requires_empty[],
388        (session_path)
389            requires[] requires_empty[session_entry_point, session_version, has_session_bytes],
390        (has_session_bytes)
391            requires[] requires_empty[session_entry_point, session_version, session_path],
392        (is_session_transfer)
393            requires[] requires_empty[session_entry_point, session_version]
394    );
395
396    check_no_conflicting_arg_types("parse_session_info", session_args_simple, session_args_json)?;
397
398    let session_args = args_from_simple_or_json(
399        arg_simple::session::parse(session_args_simple)?,
400        args_json::session::parse(session_args_json)?,
401        session_chunked_args.map(ToOwned::to_owned),
402    );
403
404    if session_transfer {
405        let session_args = session_args.as_named().unwrap().clone();
406        if session_args.is_empty() {
407            return Err(CliError::InvalidArgument {
408                context: "is_session_transfer",
409                error: "requires --session-arg to be present".to_string(),
410            });
411        }
412        return Ok(ExecutableDeployItem::Transfer { args: session_args });
413    }
414    let invalid_entry_point = || CliError::InvalidArgument {
415        context: "session_entry_point",
416        error: session_entry_point.to_string(),
417    };
418    if let Some(session_name) = name(session_name) {
419        let session_args = session_args.as_named().unwrap().clone();
420
421        return Ok(ExecutableDeployItem::StoredContractByName {
422            name: session_name,
423            entry_point: entry_point(session_entry_point).ok_or_else(invalid_entry_point)?,
424            args: session_args,
425        });
426    }
427
428    if let Some(session_hash) = contract_hash(session_hash)? {
429        let session_args = session_args.as_named().unwrap().clone();
430        return Ok(ExecutableDeployItem::StoredContractByHash {
431            hash: session_hash.into(),
432            entry_point: entry_point(session_entry_point).ok_or_else(invalid_entry_point)?,
433            args: session_args,
434        });
435    }
436
437    let version = version(session_version)?;
438    if let Some(package_name) = name(session_package_name) {
439        let session_args = session_args.as_named().unwrap().clone();
440        return Ok(ExecutableDeployItem::StoredVersionedContractByName {
441            name: package_name,
442            version, // defaults to highest enabled version
443            entry_point: entry_point(session_entry_point).ok_or_else(invalid_entry_point)?,
444            args: session_args,
445        });
446    }
447
448    if let Some(package_hash) = contract_hash(session_package_hash)? {
449        let session_args = session_args.as_named().unwrap().clone();
450        return Ok(ExecutableDeployItem::StoredVersionedContractByHash {
451            hash: package_hash.into(),
452            version, // defaults to highest enabled version
453            entry_point: entry_point(session_entry_point).ok_or_else(invalid_entry_point)?,
454            args: session_args,
455        });
456    }
457
458    let module_bytes = if !session_bytes.is_empty() {
459        session_bytes
460    } else {
461        #[cfg(feature = "std-fs-io")]
462        {
463            transaction_module_bytes(session_path)?
464        }
465        #[cfg(not(feature = "std-fs-io"))]
466        return Err(CliError::InvalidArgument {
467            context: "session_executable_deploy_item",
468            error: "missing session bytes".to_string(),
469        });
470    };
471
472    let args = session_args
473        .as_named()
474        .ok_or(CliError::UnexpectedTransactionArgsVariant)?;
475
476    Ok(ExecutableDeployItem::ModuleBytes {
477        module_bytes,
478        args: args.clone(),
479    })
480}
481
482/// Parse a transaction file into Bytes to be used in crafting a new session transaction
483pub fn transaction_module_bytes(session_path: &str) -> Result<Bytes, CliError> {
484    let module_bytes = fs::read(session_path).map_err(|error| crate::Error::IoError {
485        context: format!("unable to read session file at '{}'", session_path),
486        error,
487    })?;
488    Ok(Bytes::from(module_bytes))
489}
490
491/// Parses transfer target from a string for use with the transaction builder
492pub fn transfer_target(target_str: &str) -> Result<TransferTarget, CliError> {
493    if let Ok(public_key) = PublicKey::from_hex(target_str) {
494        return Ok(TransferTarget::PublicKey(public_key));
495    }
496    #[cfg(feature = "std-fs-io")]
497    {
498        if let Ok(public_key) = PublicKey::from_file(target_str) {
499            return Ok(TransferTarget::PublicKey(public_key));
500        }
501    }
502    if let Ok(account_hash) = AccountHash::from_formatted_str(target_str) {
503        return Ok(TransferTarget::AccountHash(account_hash));
504    }
505    if let Ok(uref) = URef::from_formatted_str(target_str) {
506        return Ok(TransferTarget::URef(uref));
507    }
508    Err(CliError::FailedToParseTransferTarget)
509}
510
511/// Parses a URef from a formatted string for the purposes of creating transactions.
512pub fn uref(uref_str: &str) -> Result<URef, CliError> {
513    match URef::from_formatted_str(uref_str) {
514        Ok(uref) => Ok(uref),
515        Err(err) => Err(CliError::FailedToParseURef {
516            context: "Failed to parse URef for transaction",
517            error: err,
518        }),
519    }
520}
521
522pub(super) fn payment_executable_deploy_item(
523    params: PaymentStrParams,
524) -> Result<ExecutableDeployItem, CliError> {
525    let PaymentStrParams {
526        payment_amount,
527        payment_hash,
528        payment_name,
529        payment_package_hash,
530        payment_package_name,
531        payment_path,
532        payment_bytes,
533        ref payment_args_simple,
534        payment_args_json,
535        payment_version,
536        payment_entry_point,
537    } = params;
538    // This is to make sure that we're using &str consistently in the macro call below.
539    let has_payment_bytes = if payment_bytes.is_empty() { "" } else { "true" };
540    check_exactly_one_not_empty!(
541        context: "parse_payment_info",
542        (payment_amount)
543            requires[] requires_empty[payment_entry_point, payment_version],
544        (payment_hash)
545            requires[payment_entry_point] requires_empty[payment_version],
546        (payment_name)
547            requires[payment_entry_point] requires_empty[payment_version],
548        (payment_package_hash)
549            requires[payment_entry_point] requires_empty[],
550        (payment_package_name)
551            requires[payment_entry_point] requires_empty[],
552        (payment_path) requires[] requires_empty[payment_entry_point, payment_version, has_payment_bytes],
553        (has_payment_bytes)
554            requires[] requires_empty[payment_entry_point, payment_version, payment_path],
555    );
556
557    check_no_conflicting_arg_types("parse_payment_info", payment_args_simple, payment_args_json)?;
558
559    let payment_args = args_from_simple_or_json(
560        arg_simple::payment::parse(payment_args_simple)?,
561        args_json::payment::parse(payment_args_json)?,
562        None,
563    );
564
565    if let Ok(payment_args) = standard_payment(payment_amount) {
566        return Ok(ExecutableDeployItem::ModuleBytes {
567            module_bytes: vec![].into(),
568            args: payment_args,
569        });
570    }
571
572    let invalid_entry_point = || CliError::InvalidArgument {
573        context: "payment_entry_point",
574        error: payment_entry_point.to_string(),
575    };
576
577    let payment_args = payment_args
578        .as_named()
579        .cloned()
580        .ok_or(CliError::UnexpectedTransactionArgsVariant)?;
581
582    if let Some(payment_name) = name(payment_name) {
583        return Ok(ExecutableDeployItem::StoredContractByName {
584            name: payment_name,
585            entry_point: entry_point(payment_entry_point).ok_or_else(invalid_entry_point)?,
586            args: payment_args,
587        });
588    }
589
590    if let Some(payment_hash) = contract_hash(payment_hash)? {
591        return Ok(ExecutableDeployItem::StoredContractByHash {
592            hash: payment_hash.into(),
593            entry_point: entry_point(payment_entry_point).ok_or_else(invalid_entry_point)?,
594            args: payment_args,
595        });
596    }
597
598    let version = version(payment_version)?;
599    if let Some(package_name) = name(payment_package_name) {
600        return Ok(ExecutableDeployItem::StoredVersionedContractByName {
601            name: package_name,
602            version, // defaults to highest enabled version
603            entry_point: entry_point(payment_entry_point).ok_or_else(invalid_entry_point)?,
604            args: payment_args,
605        });
606    }
607
608    if let Some(package_hash) = contract_hash(payment_package_hash)? {
609        return Ok(ExecutableDeployItem::StoredVersionedContractByHash {
610            hash: package_hash.into(),
611            version, // defaults to highest enabled version
612            entry_point: entry_point(payment_entry_point).ok_or_else(invalid_entry_point)?,
613            args: payment_args,
614        });
615    }
616
617    let module_bytes = fs::read(payment_path).map_err(|error| crate::Error::IoError {
618        context: format!("unable to read payment file at '{}'", payment_path),
619        error,
620    })?;
621    Ok(ExecutableDeployItem::ModuleBytes {
622        module_bytes: module_bytes.into(),
623        args: payment_args,
624    })
625}
626
627fn contract_hash(value: &str) -> Result<Option<HashAddr>, CliError> {
628    if value.is_empty() {
629        return Ok(None);
630    }
631
632    match Digest::from_hex(value) {
633        Ok(digest) => Ok(Some(digest.value())),
634        Err(error) => match Key::from_formatted_str(value) {
635            Ok(Key::Hash(hash)) | Ok(Key::SmartContract(hash)) => Ok(Some(hash)),
636            _ => Err(CliError::FailedToParseDigest {
637                context: "contract hash",
638                error,
639            }),
640        },
641    }
642}
643
644fn name(value: &str) -> Option<String> {
645    if value.is_empty() {
646        return None;
647    }
648    Some(value.to_string())
649}
650
651fn entry_point(value: &str) -> Option<String> {
652    if value.is_empty() {
653        return None;
654    }
655    Some(value.to_string())
656}
657
658fn version(value: &str) -> Result<Option<u32>, CliError> {
659    if value.is_empty() {
660        return Ok(None);
661    }
662    let parsed = value
663        .parse::<u32>()
664        .map_err(|error| CliError::FailedToParseInt {
665            context: "version",
666            error,
667        })?;
668    Ok(Some(parsed))
669}
670
671pub(super) fn transfer_id(value: &str) -> Result<u64, CliError> {
672    value.parse().map_err(|error| CliError::FailedToParseInt {
673        context: "transfer_id",
674        error,
675    })
676}
677
678pub(super) fn block_identifier(
679    maybe_block_identifier: &str,
680) -> Result<Option<BlockIdentifier>, CliError> {
681    if maybe_block_identifier.is_empty() {
682        return Ok(None);
683    }
684
685    if maybe_block_identifier.len() == (Digest::LENGTH * 2) {
686        let hash = Digest::from_hex(maybe_block_identifier).map_err(|error| {
687            CliError::FailedToParseDigest {
688                context: "block_identifier",
689                error,
690            }
691        })?;
692        Ok(Some(BlockIdentifier::Hash(BlockHash::new(hash))))
693    } else {
694        let height =
695            maybe_block_identifier
696                .parse()
697                .map_err(|error| CliError::FailedToParseInt {
698                    context: "block_identifier",
699                    error,
700                })?;
701        Ok(Some(BlockIdentifier::Height(height)))
702    }
703}
704
705pub(super) fn deploy_hash(deploy_hash: &str) -> Result<DeployHash, CliError> {
706    let hash = Digest::from_hex(deploy_hash).map_err(|error| CliError::FailedToParseDigest {
707        context: "deploy hash",
708        error,
709    })?;
710    Ok(DeployHash::new(hash))
711}
712
713pub(super) fn key_for_query(key: &str) -> Result<Key, CliError> {
714    match Key::from_formatted_str(key) {
715        Ok(key) => Ok(key),
716        Err(error) => {
717            if let Ok(public_key) = PublicKey::from_hex(key) {
718                Ok(Key::Account(public_key.to_account_hash()))
719            } else {
720                Err(CliError::FailedToParseKey {
721                    context: "key for query",
722                    error,
723                })
724            }
725        }
726    }
727}
728
729/// `maybe_block_id` can be either a block hash or a block height.
730pub(super) fn global_state_identifier(
731    maybe_block_id: &str,
732    maybe_state_root_hash: &str,
733) -> Result<Option<GlobalStateIdentifier>, CliError> {
734    match block_identifier(maybe_block_id)? {
735        Some(BlockIdentifier::Hash(hash)) => {
736            return Ok(Some(GlobalStateIdentifier::BlockHash(hash)))
737        }
738        Some(BlockIdentifier::Height(height)) => {
739            return Ok(Some(GlobalStateIdentifier::BlockHeight(height)))
740        }
741        None => (),
742    }
743
744    if maybe_state_root_hash.is_empty() {
745        return Ok(None);
746    }
747
748    let state_root_hash =
749        Digest::from_hex(maybe_state_root_hash).map_err(|error| CliError::FailedToParseDigest {
750            context: "state root hash in global_state_identifier",
751            error,
752        })?;
753    Ok(Some(GlobalStateIdentifier::StateRootHash(state_root_hash)))
754}
755
756/// `purse_id` can be a formatted public key, account hash, or URef.  It may not be empty.
757pub fn purse_identifier(purse_id: &str) -> Result<PurseIdentifier, CliError> {
758    const ACCOUNT_HASH_PREFIX: &str = "account-hash-";
759    const UREF_PREFIX: &str = "uref-";
760    const ENTITY_PREFIX: &str = "entity-";
761
762    if purse_id.is_empty() {
763        return Err(CliError::InvalidArgument {
764            context: "purse_identifier",
765            error: "cannot be empty string".to_string(),
766        });
767    }
768
769    if purse_id.starts_with(ACCOUNT_HASH_PREFIX) {
770        let account_hash = AccountHash::from_formatted_str(purse_id).map_err(|error| {
771            CliError::FailedToParseAccountHash {
772                context: "purse_identifier",
773                error,
774            }
775        })?;
776        return Ok(PurseIdentifier::MainPurseUnderAccountHash(account_hash));
777    }
778
779    if purse_id.starts_with(ENTITY_PREFIX) {
780        let entity_addr = EntityAddr::from_formatted_str(purse_id).map_err(|error| {
781            CliError::FailedToParseAddressableEntityHash {
782                context: "purse_identifier",
783                error,
784            }
785        })?;
786        return Ok(PurseIdentifier::MainPurseUnderEntityAddr(entity_addr));
787    }
788
789    if purse_id.starts_with(UREF_PREFIX) {
790        let uref =
791            URef::from_formatted_str(purse_id).map_err(|error| CliError::FailedToParseURef {
792                context: "purse_identifier",
793                error,
794            })?;
795        return Ok(PurseIdentifier::PurseUref(uref));
796    }
797
798    let public_key =
799        PublicKey::from_hex(purse_id).map_err(|error| CliError::FailedToParsePublicKey {
800            context: "purse_identifier".to_string(),
801            error,
802        })?;
803    Ok(PurseIdentifier::MainPurseUnderPublicKey(public_key))
804}
805
806/// `account_identifier` can be a formatted public key, in the form of a hex-formatted string,
807/// a pem file, or a file containing a hex formatted string, or a formatted string representing
808/// an account hash.  It may not be empty.
809pub fn account_identifier(account_identifier: &str) -> Result<AccountIdentifier, CliError> {
810    const ACCOUNT_HASH_PREFIX: &str = "account-hash-";
811
812    if account_identifier.is_empty() {
813        return Err(CliError::InvalidArgument {
814            context: "account_identifier",
815            error: "cannot be empty string".to_string(),
816        });
817    }
818
819    if account_identifier.starts_with(ACCOUNT_HASH_PREFIX) {
820        let account_hash =
821            AccountHash::from_formatted_str(account_identifier).map_err(|error| {
822                CliError::FailedToParseAccountHash {
823                    context: "account_identifier",
824                    error,
825                }
826            })?;
827        return Ok(AccountIdentifier::AccountHash(account_hash));
828    }
829
830    let public_key = PublicKey::from_hex(account_identifier).map_err(|error| {
831        CliError::FailedToParsePublicKey {
832            context: "account_identifier".to_string(),
833            error,
834        }
835    })?;
836    Ok(AccountIdentifier::PublicKey(public_key))
837}
838
839/// `entity_identifier` can be a formatted public key, in the form of a hex-formatted string,
840/// a pem file, or a file containing a hex formatted string, or a formatted string representing
841/// an account hash.  It may not be empty.
842pub fn entity_identifier(entity_identifier: &str) -> Result<EntityIdentifier, CliError> {
843    const ENTITY_PREFIX: &str = "entity-";
844    const ACCOUNT_HASH_PREFIX: &str = "account-hash-";
845
846    if entity_identifier.is_empty() {
847        return Err(CliError::InvalidArgument {
848            context: "entity_identifier",
849            error: "cannot be empty string".to_string(),
850        });
851    }
852
853    if entity_identifier.starts_with(ACCOUNT_HASH_PREFIX) {
854        let account_hash = AccountHash::from_formatted_str(entity_identifier).map_err(|error| {
855            CliError::FailedToParseAccountHash {
856                context: "entity_identifier",
857                error,
858            }
859        })?;
860        return Ok(EntityIdentifier::AccountHash(account_hash));
861    }
862    if entity_identifier.starts_with(ENTITY_PREFIX) {
863        let entity_addr = EntityAddr::from_formatted_str(entity_identifier).map_err(|error| {
864            CliError::FailedToParseAddressableEntityHash {
865                context: "entity_identifier",
866                error,
867            }
868        })?;
869        return Ok(EntityIdentifier::EntityAddr(entity_addr));
870    }
871
872    let public_key = PublicKey::from_hex(entity_identifier).map_err(|error| {
873        CliError::FailedToParsePublicKey {
874            context: "entity_identifier".to_string(),
875            error,
876        }
877    })?;
878    Ok(EntityIdentifier::PublicKey(public_key))
879}
880
881/// `era_identifier` must be an integer representing the era ID.
882pub(super) fn era_identifier(era_identifier: &str) -> Result<Option<EraIdentifier>, CliError> {
883    if era_identifier.is_empty() {
884        return Ok(None);
885    }
886    let era_id = era_identifier
887        .parse()
888        .map_err(|error| CliError::FailedToParseInt {
889            context: "era_identifier",
890            error,
891        })?;
892    Ok(Some(EraIdentifier::Era(era_id)))
893}
894
895/// `public_key` must be a public key formatted as a hex-encoded string,
896pub(super) fn public_key(public_key: &str) -> Result<Option<PublicKey>, CliError> {
897    if public_key.is_empty() {
898        return Ok(None);
899    }
900    let key =
901        PublicKey::from_hex(public_key).map_err(|error| CliError::FailedToParsePublicKey {
902            context: "public_key".to_owned(),
903            error,
904        })?;
905    Ok(Some(key))
906}
907
908pub(super) fn pricing_mode(
909    pricing_mode_identifier_str: &str,
910    payment_amount_str: &str,
911    gas_price_tolerance_str: &str,
912    additional_computation_factor_str: &str,
913    standard_payment_str: &str,
914    maybe_receipt: Option<Digest>,
915) -> Result<PricingMode, CliError> {
916    match pricing_mode_identifier_str.to_lowercase().as_str() {
917        "classic" => {
918            if gas_price_tolerance_str.is_empty() {
919                return Err(CliError::InvalidArgument {
920                    context: "gas_price_tolerance",
921                    error: "Gas price tolerance is required".to_string(),
922                });
923            }
924            if payment_amount_str.is_empty() {
925                return Err(CliError::InvalidArgument {
926                    context: "payment_amount",
927                    error: "Payment amount is required".to_string(),
928                });
929            }
930            if standard_payment_str.is_empty() {
931                return Err(CliError::InvalidArgument {
932                    context: "standard_payment",
933                    error: "Standard payment flag is required".to_string(),
934                });
935            }
936            let gas_price_tolerance = gas_price_tolerance_str.parse::<u8>().map_err(|error| {
937                CliError::FailedToParseInt {
938                    context: "gas_price_tolerance",
939                    error,
940                }
941            })?;
942            let payment_amount =
943                payment_amount_str
944                    .parse::<u64>()
945                    .map_err(|error| CliError::FailedToParseInt {
946                        context: "payment_amount",
947                        error,
948                    })?;
949            let standard_payment = standard_payment_str.parse::<bool>().map_err(|error| {
950                CliError::FailedToParseBool {
951                    context: "standard_payment",
952                    error,
953                }
954            })?;
955            Ok(PricingMode::PaymentLimited {
956                payment_amount,
957                gas_price_tolerance,
958                standard_payment,
959            })
960        }
961        "fixed" => {
962            if gas_price_tolerance_str.is_empty() {
963                return Err(CliError::InvalidArgument {
964                    context: "gas_price_tolerance",
965                    error: "Gas price tolerance is required".to_string(),
966                });
967            }
968            let gas_price_tolerance = gas_price_tolerance_str.parse::<u8>().map_err(|error| {
969                CliError::FailedToParseInt {
970                    context: "gas_price_tolerance",
971                    error,
972                }
973            })?;
974
975            // Additional Computation Factor defaults to 0 if the string is empty
976            let additional_computation_factor = if additional_computation_factor_str.is_empty() {
977                u8::default()
978            } else {
979                additional_computation_factor_str
980                    .parse::<u8>()
981                    .map_err(|error| CliError::FailedToParseInt {
982                        context: "additional_computation_factor",
983                        error,
984                    })?
985            };
986            Ok(PricingMode::Fixed {
987                gas_price_tolerance,
988                additional_computation_factor,
989            })
990        }
991        "reserved" => {
992            if maybe_receipt.is_none() {
993                return Err(CliError::InvalidArgument {
994                    context: "receipt",
995                    error: "Receipt is required for reserved pricing mode".to_string(),
996                });
997            }
998            Ok(PricingMode::Prepaid {
999                receipt: maybe_receipt.unwrap_or_default(),
1000            })
1001        }
1002        _ => Err(CliError::InvalidArgument {
1003            context: "pricing_mode",
1004            error: "Invalid pricing mode identifier".to_string(),
1005        }),
1006    }
1007}
1008
1009pub(super) fn transaction_hash(transaction_hash: &str) -> Result<TransactionHash, CliError> {
1010    let digest =
1011        Digest::from_hex(transaction_hash).map_err(|error| CliError::FailedToParseDigest {
1012            context: "failed to parse digest from string for transaction hash",
1013            error,
1014        })?;
1015    Ok(TransactionHash::from(TransactionV1Hash::from(digest)))
1016}
1017
1018#[cfg(test)]
1019mod tests {
1020    use std::convert::TryFrom;
1021
1022    use super::*;
1023
1024    const HASH: &str = "09dcee4b212cfd53642ab323fbef07dafafc6f945a80a00147f62910a915c4e6";
1025    const NAME: &str = "name";
1026    const PACKAGE_HASH: &str = "09dcee4b212cfd53642ab323fbef07dafafc6f945a80a00147f62910a915c4e6";
1027    const PACKAGE_NAME: &str = "package_name";
1028    const PATH: &str = "./session.wasm";
1029    const ENTRY_POINT: &str = "entrypoint";
1030    const VERSION: &str = "3";
1031    const TRANSFER: bool = true;
1032
1033    impl<'a> TryFrom<SessionStrParams<'a>> for ExecutableDeployItem {
1034        type Error = CliError;
1035
1036        fn try_from(params: SessionStrParams<'a>) -> Result<ExecutableDeployItem, Self::Error> {
1037            session_executable_deploy_item(params)
1038        }
1039    }
1040
1041    impl<'a> TryFrom<PaymentStrParams<'a>> for ExecutableDeployItem {
1042        type Error = CliError;
1043
1044        fn try_from(params: PaymentStrParams<'a>) -> Result<ExecutableDeployItem, Self::Error> {
1045            payment_executable_deploy_item(params)
1046        }
1047    }
1048
1049    #[test]
1050    fn should_fail_to_parse_conflicting_arg_types() {
1051        let test_context = "parse_session_info args conflict (simple json)".to_string();
1052        let actual_error = session_executable_deploy_item(SessionStrParams {
1053            session_hash: "",
1054            session_name: "name",
1055            session_package_hash: "",
1056            session_package_name: "",
1057            session_path: "",
1058            session_bytes: Bytes::new(),
1059            session_args_simple: vec!["something:u32='0'"],
1060            session_args_json: "{\"name\":\"entry_point_name\",\"type\":\"Bool\",\"value\":false}",
1061            session_version: "",
1062            session_entry_point: "entrypoint",
1063            is_session_transfer: false,
1064            session_chunked_args: None,
1065        })
1066        .unwrap_err();
1067
1068        assert!(
1069            matches!(actual_error, CliError::ConflictingArguments { ref context, .. } if *context == test_context),
1070            "{:?}",
1071            actual_error
1072        );
1073
1074        let test_context = "parse_payment_info args conflict (simple json)";
1075        let actual_error = payment_executable_deploy_item(PaymentStrParams {
1076            payment_amount: "",
1077            payment_hash: "name",
1078            payment_name: "",
1079            payment_package_hash: "",
1080            payment_package_name: "",
1081            payment_path: "",
1082            payment_bytes: Bytes::new(),
1083            payment_args_simple: vec!["something:u32='0'"],
1084            payment_args_json: "{\"name\":\"entry_point_name\",\"type\":\"Bool\",\"value\":false}",
1085            payment_version: "",
1086            payment_entry_point: "entrypoint",
1087        })
1088        .unwrap_err();
1089        assert!(
1090            matches!(
1091                actual_error,
1092                CliError::ConflictingArguments { ref context, .. } if context == test_context
1093            ),
1094            "{:?}",
1095            actual_error
1096        );
1097    }
1098
1099    #[test]
1100    fn should_fail_to_parse_conflicting_session_parameters() {
1101        let test_context = String::from("parse_session_info");
1102        assert!(matches!(
1103            session_executable_deploy_item(SessionStrParams {
1104                session_hash: HASH,
1105                session_name: NAME,
1106                session_package_hash: PACKAGE_HASH,
1107                session_package_name: PACKAGE_NAME,
1108                session_path: PATH,
1109                session_bytes: Bytes::new(),
1110                session_args_simple: vec![],
1111                session_args_json: "",
1112                session_version: "",
1113                session_entry_point: "",
1114                is_session_transfer: false,
1115                session_chunked_args: None,
1116            }),
1117            Err(CliError::ConflictingArguments { context, .. }) if context == test_context
1118        ));
1119    }
1120
1121    #[test]
1122    fn should_fail_to_parse_conflicting_payment_parameters() {
1123        let test_context = String::from("parse_payment_info");
1124        assert!(matches!(
1125            payment_executable_deploy_item(PaymentStrParams {
1126                payment_amount: "12345",
1127                payment_hash: HASH,
1128                payment_name: NAME,
1129                payment_package_hash: PACKAGE_HASH,
1130                payment_package_name: PACKAGE_NAME,
1131                payment_path: PATH,
1132                payment_bytes: Bytes::new(),
1133                payment_args_simple: vec![],
1134                payment_args_json: "",
1135                payment_version: "",
1136                payment_entry_point: "",
1137            }),
1138            Err(CliError::ConflictingArguments { context, .. }) if context == test_context
1139        ));
1140    }
1141
1142    mod missing_args {
1143        use super::*;
1144
1145        #[test]
1146        fn session_name_should_fail_to_parse_missing_entry_point() {
1147            let result = session_executable_deploy_item(SessionStrParams {
1148                session_name: NAME,
1149                ..Default::default()
1150            });
1151
1152            assert!(matches!(
1153                result,
1154                Err(CliError::InvalidArgument {
1155                    context: "parse_session_info",
1156                    ..
1157                })
1158            ));
1159        }
1160
1161        #[test]
1162        fn session_hash_should_fail_to_parse_missing_entry_point() {
1163            let result = session_executable_deploy_item(SessionStrParams {
1164                session_hash: HASH,
1165                ..Default::default()
1166            });
1167
1168            assert!(matches!(
1169                result,
1170                Err(CliError::InvalidArgument {
1171                    context: "parse_session_info",
1172                    ..
1173                })
1174            ));
1175        }
1176
1177        #[test]
1178        fn session_package_hash_should_fail_to_parse_missing_entry_point() {
1179            let result = session_executable_deploy_item(SessionStrParams {
1180                session_package_hash: PACKAGE_HASH,
1181                ..Default::default()
1182            });
1183
1184            assert!(matches!(
1185                result,
1186                Err(CliError::InvalidArgument {
1187                    context: "parse_session_info",
1188                    ..
1189                })
1190            ));
1191        }
1192
1193        #[test]
1194        fn session_package_name_should_fail_to_parse_missing_entry_point() {
1195            let result = session_executable_deploy_item(SessionStrParams {
1196                session_package_name: PACKAGE_NAME,
1197                ..Default::default()
1198            });
1199
1200            assert!(matches!(
1201                result,
1202                Err(CliError::InvalidArgument {
1203                    context: "parse_session_info",
1204                    ..
1205                })
1206            ));
1207        }
1208
1209        #[test]
1210        fn payment_name_should_fail_to_parse_missing_entry_point() {
1211            let result = payment_executable_deploy_item(PaymentStrParams {
1212                payment_name: NAME,
1213                ..Default::default()
1214            });
1215
1216            assert!(matches!(
1217                result,
1218                Err(CliError::InvalidArgument {
1219                    context: "parse_payment_info",
1220                    ..
1221                })
1222            ));
1223        }
1224
1225        #[test]
1226        fn payment_hash_should_fail_to_parse_missing_entry_point() {
1227            let result = payment_executable_deploy_item(PaymentStrParams {
1228                payment_hash: HASH,
1229                ..Default::default()
1230            });
1231
1232            assert!(matches!(
1233                result,
1234                Err(CliError::InvalidArgument {
1235                    context: "parse_payment_info",
1236                    ..
1237                })
1238            ));
1239        }
1240
1241        #[test]
1242        fn payment_package_hash_should_fail_to_parse_missing_entry_point() {
1243            let result = payment_executable_deploy_item(PaymentStrParams {
1244                payment_package_hash: PACKAGE_HASH,
1245                ..Default::default()
1246            });
1247
1248            assert!(matches!(
1249                result,
1250                Err(CliError::InvalidArgument {
1251                    context: "parse_payment_info",
1252                    ..
1253                })
1254            ));
1255        }
1256
1257        #[test]
1258        fn payment_package_name_should_fail_to_parse_missing_entry_point() {
1259            let result = payment_executable_deploy_item(PaymentStrParams {
1260                payment_package_name: PACKAGE_NAME,
1261                ..Default::default()
1262            });
1263
1264            assert!(matches!(
1265                result,
1266                Err(CliError::InvalidArgument {
1267                    context: "parse_payment_info",
1268                    ..
1269                })
1270            ));
1271        }
1272    }
1273
1274    mod conflicting_args {
1275        use super::*;
1276
1277        /// impl_test_matrix - implements many tests for SessionStrParams or PaymentStrParams which
1278        /// ensures that an error is returned when the permutation they define is executed.
1279        ///
1280        /// For instance, it is neccesary to check that when `session_path` is set, other arguments
1281        /// are not.
1282        ///
1283        /// For example, a sample invocation with one test:
1284        /// ```
1285        /// impl_test_matrix![
1286        ///     type: SessionStrParams,
1287        ///     context: "parse_session_info",
1288        ///     session_str_params[
1289        ///         test[
1290        ///             session_path => PATH,
1291        ///             conflict: session_package_hash => PACKAGE_HASH,
1292        ///             requires[],
1293        ///             path_conflicts_with_package_hash
1294        ///         ]
1295        ///     ]
1296        /// ];
1297        /// ```
1298        /// This generates the following test module (with the fn name passed), with one test per
1299        /// line in `session_str_params[]`:
1300        /// ```
1301        /// #[cfg(test)]
1302        /// mod session_str_params {
1303        ///     use super::*;
1304        ///
1305        ///     #[test]
1306        ///     fn path_conflicts_with_package_hash() {
1307        ///         let info: StdResult<ExecutableDeployItem, _> = SessionStrParams {
1308        ///                 session_path: PATH,
1309        ///                 session_package_hash: PACKAGE_HASH,
1310        ///                 ..Default::default()
1311        ///             }
1312        ///             .try_into();
1313        ///         let mut conflicting = vec![
1314        ///             format!("{}={}", "session_path", PATH),
1315        ///             format!("{}={}", "session_package_hash", PACKAGE_HASH),
1316        ///         ];
1317        ///         conflicting.sort();
1318        ///         assert!(matches!(
1319        ///             info,
1320        ///             Err(CliError::ConflictingArguments {
1321        ///                 context: "parse_session_info".to_string(),
1322        ///                 args: conflicting
1323        ///             }
1324        ///             ))
1325        ///         );
1326        ///     }
1327        /// }
1328        /// ```
1329        macro_rules! impl_test_matrix {
1330            (
1331                /// Struct for which to define the following tests. In our case, SessionStrParams or PaymentStrParams.
1332                type: $t:ident,
1333                /// Expected `context` field to be returned in the `CliError::ConflictingArguments{ context, .. }` field.
1334                context: $context:expr,
1335
1336                /// $module will be our module name.
1337                $module:ident [$(
1338                    // many tests can be defined
1339                    test[
1340                        /// The argument's ident to be tested, followed by it's value.
1341                        $arg:tt => $arg_value:expr,
1342                        /// The conflicting argument's ident to be tested, followed by it's value.
1343                        conflict: $con:tt => $con_value:expr,
1344                        /// A list of any additional fields required by the argument, and their values.
1345                        requires[$($req:tt => $req_value:expr),*],
1346                        /// fn name for the defined test.
1347                        $test_fn_name:ident
1348                    ]
1349                )+]
1350            ) => {
1351                #[cfg(test)]
1352                mod $module {
1353                    use super::*;
1354
1355                    $(
1356                        #[test]
1357                        fn $test_fn_name() {
1358                            let info: Result<ExecutableDeployItem, _> = $t {
1359                                $arg: $arg_value,
1360                                $con: $con_value,
1361                                $($req: $req_value,),*
1362                                ..Default::default()
1363                            }
1364                            .try_into();
1365                            let mut conflicting = vec![
1366                                format!("{}={}", stringify!($arg), $arg_value),
1367                                format!("{}={}", stringify!($con), $con_value),
1368                            ];
1369                            conflicting.sort();
1370                            let _context_string = $context.to_string();
1371                            assert!(matches!(
1372                                info,
1373                                Err(CliError::ConflictingArguments {
1374                                    context: _context_string,
1375                                    ..
1376                                }
1377                                ))
1378                            );
1379                        }
1380                    )+
1381                }
1382            };
1383        }
1384
1385        // NOTE: there's no need to test a conflicting argument in both directions, since they
1386        // amount to passing two fields to a structs constructor.
1387        // Where a reverse test like this is omitted, a comment should be left.
1388        impl_test_matrix![
1389            type: SessionStrParams,
1390            context: "parse_session_info",
1391            session_str_params[
1392
1393                // path
1394                test[session_path => PATH, conflict: session_package_hash => PACKAGE_HASH, requires[], path_conflicts_with_package_hash]
1395                test[session_path => PATH, conflict: session_package_name => PACKAGE_NAME, requires[], path_conflicts_with_package_name]
1396                test[session_path => PATH, conflict: session_hash =>         HASH,         requires[], path_conflicts_with_hash]
1397                test[session_path => PATH, conflict: session_name =>         HASH,         requires[], path_conflicts_with_name]
1398                test[session_path => PATH, conflict: session_version =>      VERSION,      requires[], path_conflicts_with_version]
1399                test[session_path => PATH, conflict: session_entry_point =>  ENTRY_POINT,  requires[], path_conflicts_with_entry_point]
1400                test[session_path => PATH, conflict: is_session_transfer =>  TRANSFER,     requires[], path_conflicts_with_transfer]
1401
1402                // name
1403                test[session_name => NAME, conflict: session_package_hash => PACKAGE_HASH, requires[session_entry_point => ENTRY_POINT], name_conflicts_with_package_hash]
1404                test[session_name => NAME, conflict: session_package_name => PACKAGE_NAME, requires[session_entry_point => ENTRY_POINT], name_conflicts_with_package_name]
1405                test[session_name => NAME, conflict: session_hash =>         HASH,         requires[session_entry_point => ENTRY_POINT], name_conflicts_with_hash]
1406                test[session_name => NAME, conflict: session_version =>      VERSION,      requires[session_entry_point => ENTRY_POINT], name_conflicts_with_version]
1407                test[session_name => NAME, conflict: is_session_transfer =>  TRANSFER,     requires[session_entry_point => ENTRY_POINT], name_conflicts_with_transfer]
1408
1409                // hash
1410                test[session_hash => HASH, conflict: session_package_hash => PACKAGE_HASH, requires[session_entry_point => ENTRY_POINT], hash_conflicts_with_package_hash]
1411                test[session_hash => HASH, conflict: session_package_name => PACKAGE_NAME, requires[session_entry_point => ENTRY_POINT], hash_conflicts_with_package_name]
1412                test[session_hash => HASH, conflict: session_version =>      VERSION,      requires[session_entry_point => ENTRY_POINT], hash_conflicts_with_version]
1413                test[session_hash => HASH, conflict: is_session_transfer =>  TRANSFER,     requires[session_entry_point => ENTRY_POINT], hash_conflicts_with_transfer]
1414                // name <-> hash is already checked
1415                // name <-> path is already checked
1416
1417                // package_name
1418                // package_name + session_version is optional and allowed
1419                test[session_package_name => PACKAGE_NAME, conflict: session_package_hash => PACKAGE_HASH, requires[session_entry_point => ENTRY_POINT], package_name_conflicts_with_package_hash]
1420                test[session_package_name => VERSION, conflict: is_session_transfer => TRANSFER, requires[session_entry_point => ENTRY_POINT], package_name_conflicts_with_transfer]
1421                // package_name <-> hash is already checked
1422                // package_name <-> name is already checked
1423                // package_name <-> path is already checked
1424
1425                // package_hash
1426                // package_hash + session_version is optional and allowed
1427                test[session_package_hash => PACKAGE_HASH, conflict: is_session_transfer => TRANSFER, requires[session_entry_point => ENTRY_POINT], package_hash_conflicts_with_transfer]
1428                // package_hash <-> package_name is already checked
1429                // package_hash <-> hash is already checked
1430                // package_hash <-> name is already checked
1431                // package_hash <-> path is already checked
1432
1433            ]
1434        ];
1435
1436        impl_test_matrix![
1437            type: PaymentStrParams,
1438            context: "parse_payment_info",
1439            payment_str_params[
1440
1441                // amount
1442                test[payment_amount => PATH, conflict: payment_package_hash => PACKAGE_HASH, requires[], amount_conflicts_with_package_hash]
1443                test[payment_amount => PATH, conflict: payment_package_name => PACKAGE_NAME, requires[], amount_conflicts_with_package_name]
1444                test[payment_amount => PATH, conflict: payment_hash =>         HASH,         requires[], amount_conflicts_with_hash]
1445                test[payment_amount => PATH, conflict: payment_name =>         HASH,         requires[], amount_conflicts_with_name]
1446                test[payment_amount => PATH, conflict: payment_version =>      VERSION,      requires[], amount_conflicts_with_version]
1447                test[payment_amount => PATH, conflict: payment_entry_point =>  ENTRY_POINT,  requires[], amount_conflicts_with_entry_point]
1448
1449                // path
1450                // amount <-> path is already checked
1451                test[payment_path => PATH, conflict: payment_package_hash => PACKAGE_HASH, requires[], path_conflicts_with_package_hash]
1452                test[payment_path => PATH, conflict: payment_package_name => PACKAGE_NAME, requires[], path_conflicts_with_package_name]
1453                test[payment_path => PATH, conflict: payment_hash =>         HASH,         requires[], path_conflicts_with_hash]
1454                test[payment_path => PATH, conflict: payment_name =>         HASH,         requires[], path_conflicts_with_name]
1455                test[payment_path => PATH, conflict: payment_version =>      VERSION,      requires[], path_conflicts_with_version]
1456                test[payment_path => PATH, conflict: payment_entry_point =>  ENTRY_POINT,  requires[], path_conflicts_with_entry_point]
1457
1458                // name
1459                // amount <-> path is already checked
1460                test[payment_name => NAME, conflict: payment_package_hash => PACKAGE_HASH, requires[payment_entry_point => ENTRY_POINT], name_conflicts_with_package_hash]
1461                test[payment_name => NAME, conflict: payment_package_name => PACKAGE_NAME, requires[payment_entry_point => ENTRY_POINT], name_conflicts_with_package_name]
1462                test[payment_name => NAME, conflict: payment_hash =>         HASH,         requires[payment_entry_point => ENTRY_POINT], name_conflicts_with_hash]
1463                test[payment_name => NAME, conflict: payment_version =>      VERSION,      requires[payment_entry_point => ENTRY_POINT], name_conflicts_with_version]
1464
1465                // hash
1466                // amount <-> hash is already checked
1467                test[payment_hash => HASH, conflict: payment_package_hash => PACKAGE_HASH, requires[payment_entry_point => ENTRY_POINT], hash_conflicts_with_package_hash]
1468                test[payment_hash => HASH, conflict: payment_package_name => PACKAGE_NAME, requires[payment_entry_point => ENTRY_POINT], hash_conflicts_with_package_name]
1469                test[payment_hash => HASH, conflict: payment_version =>      VERSION,      requires[payment_entry_point => ENTRY_POINT], hash_conflicts_with_version]
1470                // name <-> hash is already checked
1471                // name <-> path is already checked
1472
1473                // package_name
1474                // amount <-> package_name is already checked
1475                test[payment_package_name => PACKAGE_NAME, conflict: payment_package_hash => PACKAGE_HASH, requires[payment_entry_point => ENTRY_POINT], package_name_conflicts_with_package_hash]
1476                // package_name <-> hash is already checked
1477                // package_name <-> name is already checked
1478                // package_name <-> path is already checked
1479
1480                // package_hash
1481                // package_hash + session_version is optional and allowed
1482                // amount <-> package_hash is already checked
1483                // package_hash <-> package_name is already checked
1484                // package_hash <-> hash is already checked
1485                // package_hash <-> name is already checked
1486                // package_hash <-> path is already checked
1487            ]
1488        ];
1489    }
1490
1491    mod param_tests {
1492        use super::*;
1493
1494        const HASH: &str = "09dcee4b212cfd53642ab323fbef07dafafc6f945a80a00147f62910a915c4e6";
1495        const NAME: &str = "name";
1496        const PKG_NAME: &str = "pkg_name";
1497        const PKG_HASH: &str = "09dcee4b212cfd53642ab323fbef07dafafc6f945a80a00147f62910a915c4e6";
1498        const ENTRYPOINT: &str = "entrypoint";
1499        const VERSION: &str = "4";
1500
1501        fn args_simple() -> Vec<&'static str> {
1502            vec!["name_01:bool='false'", "name_02:u32='42'"]
1503        }
1504
1505        /// Sample data creation methods for PaymentStrParams
1506        mod session_params {
1507            use std::collections::BTreeMap;
1508
1509            use casper_types::CLValue;
1510
1511            use super::*;
1512
1513            #[test]
1514            pub fn with_hash() {
1515                let params: Result<ExecutableDeployItem, CliError> =
1516                    SessionStrParams::with_hash(HASH, ENTRYPOINT, args_simple(), "").try_into();
1517                match params {
1518                    Ok(item @ ExecutableDeployItem::StoredContractByHash { .. }) => {
1519                        let actual: BTreeMap<String, CLValue> = item.args().clone().into();
1520                        let mut expected = BTreeMap::new();
1521                        expected.insert("name_01".to_owned(), CLValue::from_t(false).unwrap());
1522                        expected.insert("name_02".to_owned(), CLValue::from_t(42u32).unwrap());
1523                        assert_eq!(actual, expected);
1524                    }
1525                    other => panic!("incorrect type parsed {:?}", other),
1526                }
1527            }
1528
1529            #[test]
1530            pub fn with_name() {
1531                let params: Result<ExecutableDeployItem, CliError> =
1532                    SessionStrParams::with_name(NAME, ENTRYPOINT, args_simple(), "").try_into();
1533                match params {
1534                    Ok(item @ ExecutableDeployItem::StoredContractByName { .. }) => {
1535                        let actual: BTreeMap<String, CLValue> = item.args().clone().into();
1536                        let mut expected = BTreeMap::new();
1537                        expected.insert("name_01".to_owned(), CLValue::from_t(false).unwrap());
1538                        expected.insert("name_02".to_owned(), CLValue::from_t(42u32).unwrap());
1539                        assert_eq!(actual, expected);
1540                    }
1541                    other => panic!("incorrect type parsed {:?}", other),
1542                }
1543            }
1544
1545            #[test]
1546            pub fn with_package_name() {
1547                let params: Result<ExecutableDeployItem, CliError> =
1548                    SessionStrParams::with_package_name(
1549                        PKG_NAME,
1550                        VERSION,
1551                        ENTRYPOINT,
1552                        args_simple(),
1553                        "",
1554                    )
1555                    .try_into();
1556                match params {
1557                    Ok(item @ ExecutableDeployItem::StoredVersionedContractByName { .. }) => {
1558                        let actual: BTreeMap<String, CLValue> = item.args().clone().into();
1559                        let mut expected = BTreeMap::new();
1560                        expected.insert("name_01".to_owned(), CLValue::from_t(false).unwrap());
1561                        expected.insert("name_02".to_owned(), CLValue::from_t(42u32).unwrap());
1562                        assert_eq!(actual, expected);
1563                    }
1564                    other => panic!("incorrect type parsed {:?}", other),
1565                }
1566            }
1567
1568            #[test]
1569            pub fn with_package_hash() {
1570                let params: Result<ExecutableDeployItem, CliError> =
1571                    SessionStrParams::with_package_hash(
1572                        PKG_HASH,
1573                        VERSION,
1574                        ENTRYPOINT,
1575                        args_simple(),
1576                        "",
1577                    )
1578                    .try_into();
1579                match params {
1580                    Ok(item @ ExecutableDeployItem::StoredVersionedContractByHash { .. }) => {
1581                        let actual: BTreeMap<String, CLValue> = item.args().clone().into();
1582                        let mut expected = BTreeMap::new();
1583                        expected.insert("name_01".to_owned(), CLValue::from_t(false).unwrap());
1584                        expected.insert("name_02".to_owned(), CLValue::from_t(42u32).unwrap());
1585                        assert_eq!(actual, expected);
1586                    }
1587                    other => panic!("incorrect type parsed {:?}", other),
1588                }
1589            }
1590        }
1591        /// Sample data creation methods for PaymentStrParams
1592        mod payment_params {
1593            use std::collections::BTreeMap;
1594
1595            use casper_types::{CLValue, U512};
1596
1597            use super::*;
1598
1599            #[test]
1600            pub fn with_amount() {
1601                let params: Result<ExecutableDeployItem, CliError> =
1602                    PaymentStrParams::with_amount("100").try_into();
1603                match params {
1604                    Ok(item @ ExecutableDeployItem::ModuleBytes { .. }) => {
1605                        let amount = CLValue::from_t(U512::from(100)).unwrap();
1606                        assert_eq!(item.args().get("amount"), Some(&amount));
1607                    }
1608                    other => panic!("incorrect type parsed {:?}", other),
1609                }
1610            }
1611
1612            #[test]
1613            pub fn with_hash() {
1614                let params: Result<ExecutableDeployItem, CliError> =
1615                    PaymentStrParams::with_hash(HASH, ENTRYPOINT, args_simple(), "").try_into();
1616                match params {
1617                    Ok(item @ ExecutableDeployItem::StoredContractByHash { .. }) => {
1618                        let actual: BTreeMap<String, CLValue> = item.args().clone().into();
1619                        let mut expected = BTreeMap::new();
1620                        expected.insert("name_01".to_owned(), CLValue::from_t(false).unwrap());
1621                        expected.insert("name_02".to_owned(), CLValue::from_t(42u32).unwrap());
1622                        assert_eq!(actual, expected);
1623                    }
1624                    other => panic!("incorrect type parsed {:?}", other),
1625                }
1626            }
1627
1628            #[test]
1629            pub fn with_name() {
1630                let params: Result<ExecutableDeployItem, CliError> =
1631                    PaymentStrParams::with_name(NAME, ENTRYPOINT, args_simple(), "").try_into();
1632                match params {
1633                    Ok(item @ ExecutableDeployItem::StoredContractByName { .. }) => {
1634                        let actual: BTreeMap<String, CLValue> = item.args().clone().into();
1635                        let mut expected = BTreeMap::new();
1636                        expected.insert("name_01".to_owned(), CLValue::from_t(false).unwrap());
1637                        expected.insert("name_02".to_owned(), CLValue::from_t(42u32).unwrap());
1638                        assert_eq!(actual, expected);
1639                    }
1640                    other => panic!("incorrect type parsed {:?}", other),
1641                }
1642            }
1643
1644            #[test]
1645            pub fn with_package_name() {
1646                let params: Result<ExecutableDeployItem, CliError> =
1647                    PaymentStrParams::with_package_name(
1648                        PKG_NAME,
1649                        VERSION,
1650                        ENTRYPOINT,
1651                        args_simple(),
1652                        "",
1653                    )
1654                    .try_into();
1655                match params {
1656                    Ok(item @ ExecutableDeployItem::StoredVersionedContractByName { .. }) => {
1657                        let actual: BTreeMap<String, CLValue> = item.args().clone().into();
1658                        let mut expected = BTreeMap::new();
1659                        expected.insert("name_01".to_owned(), CLValue::from_t(false).unwrap());
1660                        expected.insert("name_02".to_owned(), CLValue::from_t(42u32).unwrap());
1661                        assert_eq!(actual, expected);
1662                    }
1663                    other => panic!("incorrect type parsed {:?}", other),
1664                }
1665            }
1666
1667            #[test]
1668            pub fn with_package_hash() {
1669                let params: Result<ExecutableDeployItem, CliError> =
1670                    PaymentStrParams::with_package_hash(
1671                        PKG_HASH,
1672                        VERSION,
1673                        ENTRYPOINT,
1674                        args_simple(),
1675                        "",
1676                    )
1677                    .try_into();
1678                match params {
1679                    Ok(item @ ExecutableDeployItem::StoredVersionedContractByHash { .. }) => {
1680                        let actual: BTreeMap<String, CLValue> = item.args().clone().into();
1681                        let mut expected = BTreeMap::new();
1682                        expected.insert("name_01".to_owned(), CLValue::from_t(false).unwrap());
1683                        expected.insert("name_02".to_owned(), CLValue::from_t(42u32).unwrap());
1684                        assert_eq!(actual, expected);
1685                    }
1686                    other => panic!("incorrect type parsed {:?}", other),
1687                }
1688            }
1689        }
1690    }
1691
1692    mod account_identifier {
1693        use super::*;
1694
1695        #[test]
1696        pub fn should_parse_valid_account_hash() {
1697            let account_hash =
1698                "account-hash-c029c14904b870e64c1d443d428c606740e82f341bea0f8542ca6494cef1383e";
1699            let parsed = account_identifier(account_hash).unwrap();
1700            let expected = AccountHash::from_formatted_str(account_hash).unwrap();
1701            assert_eq!(parsed, AccountIdentifier::AccountHash(expected));
1702        }
1703
1704        #[test]
1705        pub fn should_parse_valid_public_key() {
1706            let public_key = "01567f0f205e83291312cd82988d66143d376cee7de904dd2605d3f4bbb69b3c80";
1707            let parsed = account_identifier(public_key).unwrap();
1708            let expected = PublicKey::from_hex(public_key).unwrap();
1709            assert_eq!(parsed, AccountIdentifier::PublicKey(expected));
1710        }
1711
1712        #[test]
1713        pub fn should_fail_to_parse_invalid_account_hash() {
1714            //This is the account hash from above with several characters removed
1715            let account_hash =
1716                "account-hash-c029c14904b870e1d443d428c606740e82f341bea0f8542ca6494cef1383e";
1717            let parsed = account_identifier(account_hash);
1718            assert!(parsed.is_err());
1719        }
1720
1721        #[test]
1722        pub fn should_fail_to_parse_invalid_public_key() {
1723            //This is the public key from above with several characters removed
1724            let public_key = "01567f0f205e83291312cd82988d66143d376cee7de904dd26054bbb69b3c80";
1725            let parsed = account_identifier(public_key);
1726            assert!(parsed.is_err());
1727        }
1728    }
1729
1730    mod entity_identifier {
1731        use super::*;
1732
1733        #[test]
1734        pub fn should_parse_valid_contract_entity_addr() {
1735            let entity_addr =
1736                "entity-contract-c029c14904b870e64c1d443d428c606740e82f341bea0f8542ca6494cef1383e";
1737            let parsed = entity_identifier(entity_addr).unwrap();
1738            assert_eq!(
1739                parsed,
1740                EntityIdentifier::EntityAddr(
1741                    EntityAddr::from_formatted_str(entity_addr).expect("should parse EntityAddr")
1742                )
1743            );
1744        }
1745
1746        #[test]
1747        pub fn should_parse_valid_account_entity_addr() {
1748            let entity_addr =
1749                "entity-account-c029c14904b870e64c1d443d428c606740e82f341bea0f8542ca6494cef1383e";
1750            let parsed = entity_identifier(entity_addr).unwrap();
1751            assert_eq!(
1752                parsed,
1753                EntityIdentifier::EntityAddr(
1754                    EntityAddr::from_formatted_str(entity_addr).expect("should parse EntityAddr")
1755                )
1756            );
1757        }
1758
1759        #[test]
1760        pub fn should_parse_valid_public_key() {
1761            let public_key = "01567f0f205e83291312cd82988d66143d376cee7de904dd2605d3f4bbb69b3c80";
1762            let parsed = entity_identifier(public_key).unwrap();
1763            let expected = PublicKey::from_hex(public_key).unwrap();
1764            assert_eq!(parsed, EntityIdentifier::PublicKey(expected));
1765        }
1766
1767        #[test]
1768        pub fn should_fail_to_parse_invalid_entity_hash() {
1769            //This is the account hash from above with several characters removed
1770            let entity_hash =
1771                "contract-addressable-entity-c029c14904b870e64c1d443d428c606740e82f341bea0f8542ca6494cef138";
1772            let parsed = entity_identifier(entity_hash);
1773            assert!(parsed.is_err());
1774        }
1775
1776        #[test]
1777        pub fn should_fail_to_parse_invalid_public_key() {
1778            //This is the public key from above with several characters removed
1779            let public_key = "01567f0f205e83291312cd82988d66143d376cee7de904dd26054bbb69b3c80";
1780            let parsed = entity_identifier(public_key);
1781            assert!(parsed.is_err());
1782        }
1783    }
1784
1785    mod era_identifier {
1786        use casper_types::EraId;
1787
1788        use super::*;
1789
1790        #[test]
1791        pub fn should_parse_valid_era_id() {
1792            let era_id = "123";
1793            let parsed = era_identifier(era_id).unwrap();
1794            assert!(
1795                matches!(parsed, Some(EraIdentifier::Era(id)) if id == EraId::new(123)),
1796                "{:?}",
1797                parsed
1798            );
1799        }
1800
1801        #[test]
1802        pub fn should_fail_to_parse_invalid_era_id() {
1803            let era_id = "invalid";
1804            let parsed = era_identifier(era_id);
1805            assert!(parsed.is_err());
1806        }
1807    }
1808
1809    mod public_key {
1810        use super::*;
1811
1812        #[test]
1813        pub fn should_parse_valid_public_key() {
1814            let str = "01567f0f205e83291312cd82988d66143d376cee7de904dd2605d3f4bbb69b3c80";
1815            let parsed = public_key(str).unwrap();
1816            let expected = PublicKey::from_hex(str).unwrap();
1817            assert_eq!(parsed, Some(expected));
1818        }
1819
1820        #[test]
1821        pub fn should_fail_to_parse_invalid_public_key() {
1822            //This is the public key from above with several characters removed
1823            let str = "01567f0f205e83291312cd82988d66143d376cee7de904dd26054bbb69b3c80";
1824            let parsed = public_key(str);
1825            assert!(parsed.is_err());
1826        }
1827    }
1828
1829    mod pricing_mode {
1830        use super::*;
1831
1832        const VALID_HASH: &str = "09dcee4b212cfd53642ab323fbef07dafafc6f945a80a00147f62910a915c4e6";
1833        #[test]
1834        fn should_parse_fixed_pricing_mode_identifier() {
1835            let pricing_mode_str = "fixed";
1836            let payment_amount = "";
1837            let gas_price_tolerance = "10";
1838            let additional_computation_factor = "1";
1839            let standard_payment = "";
1840            let parsed = pricing_mode(
1841                pricing_mode_str,
1842                payment_amount,
1843                gas_price_tolerance,
1844                additional_computation_factor,
1845                standard_payment,
1846                None,
1847            )
1848            .unwrap();
1849            assert_eq!(
1850                parsed,
1851                PricingMode::Fixed {
1852                    additional_computation_factor: 1,
1853                    gas_price_tolerance: 10,
1854                }
1855            );
1856        }
1857
1858        #[test]
1859        fn should_parse_fixed_pricing_mode_identifier_without_additional_computation_factor() {
1860            let pricing_mode_str = "fixed";
1861            let payment_amount = "";
1862            let gas_price_tolerance = "10";
1863            let additional_computation_factor = "";
1864            let standard_payment = "";
1865            let parsed = pricing_mode(
1866                pricing_mode_str,
1867                payment_amount,
1868                gas_price_tolerance,
1869                additional_computation_factor,
1870                standard_payment,
1871                None,
1872            )
1873            .unwrap();
1874            assert_eq!(
1875                parsed,
1876                PricingMode::Fixed {
1877                    additional_computation_factor: 0,
1878                    gas_price_tolerance: 10,
1879                }
1880            );
1881        }
1882
1883        #[test]
1884        fn should_parse_reserved_pricing_mode() {
1885            let pricing_mode_str = "reserved";
1886            let payment_amount = "";
1887            let gas_price_tolerance = "";
1888            let additional_computation_factor = "0";
1889            let standard_payment = "";
1890            let parsed = pricing_mode(
1891                pricing_mode_str,
1892                payment_amount,
1893                gas_price_tolerance,
1894                additional_computation_factor,
1895                standard_payment,
1896                Some(Digest::from_hex(VALID_HASH).unwrap()),
1897            )
1898            .unwrap();
1899            assert_eq!(
1900                parsed,
1901                PricingMode::Prepaid {
1902                    receipt: Digest::from_hex(VALID_HASH).unwrap(),
1903                }
1904            );
1905        }
1906        #[test]
1907        fn should_parse_classic_pricing_mode() {
1908            let pricing_mode_str = "classic";
1909            let payment_amount = "10";
1910            let standard_payment = "true";
1911            let gas_price_tolerance = "10";
1912            let additional_computation_factor = "0";
1913            let parsed = pricing_mode(
1914                pricing_mode_str,
1915                payment_amount,
1916                gas_price_tolerance,
1917                additional_computation_factor,
1918                standard_payment,
1919                None,
1920            )
1921            .unwrap();
1922            assert_eq!(
1923                parsed,
1924                PricingMode::PaymentLimited {
1925                    payment_amount: 10,
1926                    gas_price_tolerance: 10,
1927                    standard_payment: true,
1928                }
1929            );
1930        }
1931
1932        #[test]
1933        fn should_fail_to_parse_invalid_pricing_mode() {
1934            let pricing_mode_str = "invalid";
1935            let payment_amount = "10";
1936            let standard_payment = "true";
1937            let gas_price_tolerance = "10";
1938            let additional_computation_factor = "0";
1939            let parsed = pricing_mode(
1940                pricing_mode_str,
1941                payment_amount,
1942                gas_price_tolerance,
1943                additional_computation_factor,
1944                standard_payment,
1945                None,
1946            );
1947            assert!(parsed.is_err());
1948            assert!(matches!(parsed, Err(CliError::InvalidArgument { .. })));
1949        }
1950
1951        #[test]
1952        fn should_fail_to_parse_invalid_additional_computation_factor() {
1953            let pricing_mode_str = "fixed";
1954            let payment_amount = "10";
1955            let standard_payment = "true";
1956            let gas_price_tolerance = "10";
1957            let additional_computation_factor = "invalid";
1958            let parsed = pricing_mode(
1959                pricing_mode_str,
1960                payment_amount,
1961                gas_price_tolerance,
1962                additional_computation_factor,
1963                standard_payment,
1964                None,
1965            );
1966            assert!(parsed.is_err());
1967            assert!(matches!(parsed, Err(CliError::FailedToParseInt { .. })));
1968        }
1969
1970        #[test]
1971        fn should_fail_to_parse_classic_without_amount() {
1972            let pricing_mode_str = "classic";
1973            let payment_amount = "";
1974            let standard_payment = "true";
1975            let gas_price_tolerance = "10";
1976            let additional_computation_factor = "0";
1977            let parsed = pricing_mode(
1978                pricing_mode_str,
1979                payment_amount,
1980                gas_price_tolerance,
1981                additional_computation_factor,
1982                standard_payment,
1983                None,
1984            );
1985            assert!(parsed.is_err());
1986            assert!(matches!(parsed, Err(CliError::InvalidArgument { .. })));
1987        }
1988    }
1989    mod transaction_hash {
1990        use super::*;
1991        const VALID_HASH: &str = "09dcee4b212cfd53642ab323fbef07dafafc6f945a80a00147f62910a915c4e6";
1992        const INVALID_HASH: &str =
1993            "09dcee4b212cfd53642ab323fbef07dafafc6f945a80a00147f62910a915c4e";
1994        #[test]
1995        fn should_parse_transaction_hash() {
1996            let parsed = transaction_hash(VALID_HASH);
1997            assert!(parsed.is_ok());
1998            assert_eq!(
1999                parsed.unwrap(),
2000                TransactionHash::from(TransactionV1Hash::from(
2001                    Digest::from_hex(VALID_HASH).unwrap()
2002                ))
2003            );
2004        }
2005        #[test]
2006        fn should_fail_to_parse_incorrect_hash() {
2007            let parsed = transaction_hash(INVALID_HASH);
2008            assert!(parsed.is_err());
2009            assert!(matches!(
2010                parsed,
2011                Err(CliError::FailedToParseDigest {
2012                    context: "failed to parse digest from string for transaction hash",
2013                    ..
2014                })
2015            ));
2016        }
2017    }
2018}