odra_cli/
args.rs

1use std::str::FromStr;
2
3use clap::{Arg, ArgAction, ArgMatches};
4use odra::casper_types::{CLType, CLValue, RuntimeArgs};
5use odra::schema::casper_contract_schema::{Argument, CustomType, Entrypoint, NamedCLType, Type};
6use serde_json::Value;
7use thiserror::Error;
8
9use crate::{types, CustomTypeSet};
10
11pub const ARG_ATTACHED_VALUE: &str = "__attached_value";
12
13#[derive(Debug, Error)]
14pub enum ArgsError {
15    #[error("Invalid arg value: {0}")]
16    TypesError(#[from] types::Error),
17    #[error("Decoding error: {0}")]
18    DecodingError(String),
19    #[error("Arg not found: {0}")]
20    ArgNotFound(String),
21    #[error("Arg type not found: {0}")]
22    ArgTypeNotFound(String)
23}
24
25/// A typed command argument.
26#[derive(Debug, PartialEq)]
27pub struct CommandArg {
28    pub name: String,
29    pub required: bool,
30    pub description: String,
31    pub ty: NamedCLType,
32    pub is_list_element: bool
33}
34
35impl CommandArg {
36    pub fn new(
37        name: &str,
38        description: &str,
39        ty: NamedCLType,
40        required: bool,
41        is_list_element: bool
42    ) -> Self {
43        Self {
44            name: name.to_string(),
45            required,
46            description: description.to_string(),
47            ty,
48            is_list_element
49        }
50    }
51}
52
53impl From<CommandArg> for Arg {
54    fn from(arg: CommandArg) -> Self {
55        let result = Arg::new(&arg.name)
56            .long(arg.name)
57            .value_name(format!("{:?}", arg.ty))
58            .required(arg.required)
59            .help(arg.description);
60
61        match arg.is_list_element {
62            true => result.action(ArgAction::Append),
63            false => result.action(ArgAction::Set)
64        }
65    }
66}
67
68pub fn entry_point_args(entry_point: &Entrypoint, types: &CustomTypeSet) -> Vec<Arg> {
69    entry_point
70        .arguments
71        .iter()
72        .flat_map(|arg| flat_arg(arg, types, false))
73        .flatten()
74        .map(Into::into)
75        .collect()
76}
77
78fn flat_arg(
79    arg: &Argument,
80    types: &CustomTypeSet,
81    is_list_element: bool
82) -> Result<Vec<CommandArg>, ArgsError> {
83    match &arg.ty.0 {
84        NamedCLType::Custom(name) => {
85            let matching_type = types
86                .iter()
87                .find(|ty| {
88                    let type_name = match ty {
89                        CustomType::Struct { name, .. } => &name.0,
90                        CustomType::Enum { name, .. } => &name.0
91                    };
92                    name == type_name
93                })
94                .ok_or(ArgsError::ArgTypeNotFound(name.clone()))?;
95
96            match matching_type {
97                CustomType::Struct { members, .. } => {
98                    let commands = members
99                        .iter()
100                        .map(|field| {
101                            let field_arg = Argument {
102                                name: format!("{}.{}", arg.name, field.name),
103                                ty: field.ty.clone(),
104                                optional: arg.optional,
105                                description: field.description.clone()
106                            };
107                            flat_arg(&field_arg, types, is_list_element)
108                        })
109                        .collect::<Result<Vec<_>, _>>()?;
110                    Ok(commands.into_iter().flatten().collect())
111                }
112                CustomType::Enum { variants, .. } => {
113                    let commands = variants
114                        .iter()
115                        .map(|variant| {
116                            let variant_arg = Argument {
117                                name: format!("{}.{}", arg.name, variant.name.to_lowercase()),
118                                ty: variant.ty.clone(),
119                                optional: arg.optional,
120                                description: variant.description.clone()
121                            };
122                            flat_arg(&variant_arg, types, is_list_element)
123                        })
124                        .collect::<Result<Vec<_>, _>>()?;
125                    Ok(commands.into_iter().flatten().collect())
126                }
127            }
128        }
129        NamedCLType::List(inner) => {
130            let arg = Argument {
131                ty: Type(*inner.clone()),
132                ..arg.clone()
133            };
134            flat_arg(&arg, types, true)
135        }
136        _ => Ok(vec![CommandArg::new(
137            &arg.name,
138            &arg.description.clone().unwrap_or_default(),
139            arg.ty.0.clone(),
140            !arg.optional,
141            is_list_element
142        )])
143    }
144}
145
146pub fn compose(
147    entry_point: &Entrypoint,
148    args: &ArgMatches,
149    types: &CustomTypeSet
150) -> Result<RuntimeArgs, ArgsError> {
151    let mut runtime_args = RuntimeArgs::new();
152
153    for arg in entry_point.arguments.iter() {
154        let parts: Vec<CommandArg> = flat_arg(arg, types, false)?;
155
156        let cl_value = if parts.len() == 1 {
157            let input = args
158                .get_many::<String>(&arg.name)
159                .unwrap_or_default()
160                .map(|s| s.as_str())
161                .collect::<Vec<_>>();
162            let ty = &arg.ty.0;
163            if input.is_empty() {
164                continue;
165            }
166            match ty {
167                NamedCLType::List(inner) => {
168                    let input = input
169                        .iter()
170                        .flat_map(|v| v.split(',').collect::<Vec<_>>())
171                        .collect();
172                    let bytes = types::vec_into_bytes(inner, input)?;
173                    let cl_type = CLType::List(Box::new(types::named_cl_type_to_cl_type(inner)));
174                    CLValue::from_components(cl_type, bytes)
175                }
176                _ => {
177                    let bytes = types::into_bytes(ty, input[0])?;
178                    let cl_type = types::named_cl_type_to_cl_type(ty);
179                    CLValue::from_components(cl_type, bytes)
180                }
181            }
182        } else {
183            build_complex_arg(parts, args)?
184        };
185        runtime_args.insert_cl_value(arg.name.clone(), cl_value);
186    }
187
188    Ok(runtime_args)
189}
190
191#[derive(Debug, PartialEq)]
192struct ComposedArg<'a> {
193    name: String,
194    values: Vec<Values<'a>>
195}
196type Values<'a> = (NamedCLType, Vec<&'a str>);
197
198impl<'a> ComposedArg<'a> {
199    fn new(name: &str) -> Self {
200        Self {
201            name: name.to_string(),
202            values: vec![]
203        }
204    }
205
206    fn add(&mut self, value: Values<'a>) {
207        self.values.push(value);
208    }
209
210    fn flush(&mut self, buffer: &mut Vec<u8>) -> Result<(), ArgsError> {
211        if self.values.is_empty() {
212            return Ok(());
213        }
214        let size = self.values[0].1.len();
215
216        // check if all values have the same length
217        let equals_len = self
218            .values
219            .iter()
220            .map(|(_, vec)| vec.len())
221            .all(|len| len == size);
222
223        if !equals_len {
224            return Err(ArgsError::DecodingError(format!(
225                "Not equal args length for the list `{}`",
226                self.name
227            )));
228        }
229
230        buffer.extend(types::to_bytes_or_err(size as u32)?);
231
232        for i in 0..size {
233            for (ty, values) in &self.values {
234                let bytes = types::into_bytes(ty, values[i])?;
235                buffer.extend_from_slice(&bytes);
236            }
237        }
238        self.values.clear();
239        Ok(())
240    }
241}
242
243fn build_complex_arg(args: Vec<CommandArg>, matches: &ArgMatches) -> Result<CLValue, ArgsError> {
244    let mut current_group = ComposedArg::new("");
245    let mut buffer: Vec<u8> = vec![];
246    for arg in args {
247        let args = matches
248            .get_many::<String>(&arg.name)
249            .ok_or(ArgsError::ArgNotFound(arg.name.clone()))?
250            .map(|v| v.as_str())
251            .collect::<Vec<_>>();
252        let ty = arg.ty;
253        let is_list_element = arg.is_list_element;
254
255        let parts = arg
256            .name
257            .split('.')
258            .map(|s| s.to_string())
259            .collect::<Vec<_>>();
260        let parent = parts[parts.len() - 2].clone();
261
262        if current_group.name != parent && is_list_element {
263            current_group.flush(&mut buffer)?;
264            current_group = ComposedArg::new(&parent);
265            current_group.add((ty, args));
266        } else if current_group.name == parent && is_list_element {
267            current_group.add((ty, args));
268        } else {
269            current_group.flush(&mut buffer)?;
270            let bytes = types::into_bytes(&ty, args[0])?;
271            buffer.extend_from_slice(&bytes);
272        }
273    }
274    current_group.flush(&mut buffer)?;
275    Ok(CLValue::from_components(CLType::Any, buffer))
276}
277
278pub fn decode<'a>(
279    bytes: &'a [u8],
280    ty: &Type,
281    types: &'a CustomTypeSet
282) -> Result<(String, &'a [u8]), ArgsError> {
283    match &ty.0 {
284        NamedCLType::Custom(name) => {
285            let matching_type = types
286                .iter()
287                .find(|ty| {
288                    let type_name = match ty {
289                        CustomType::Struct { name, .. } => &name.0,
290                        CustomType::Enum { name, .. } => &name.0
291                    };
292                    name == type_name
293                })
294                .ok_or(ArgsError::ArgTypeNotFound(name.clone()))?;
295            let mut bytes = bytes;
296
297            match matching_type {
298                CustomType::Struct { members, .. } => {
299                    let mut decoded = "{ ".to_string();
300                    for field in members {
301                        let (value, rem) = decode(bytes, &field.ty, types)?;
302                        decoded.push_str(format!(" \"{}\": \"{}\",", field.name, value).as_str());
303                        bytes = rem;
304                    }
305                    decoded.pop();
306                    decoded.push_str(" }");
307                    Ok((to_json(&decoded)?, bytes))
308                }
309                CustomType::Enum { variants, .. } => {
310                    let ty = Type(NamedCLType::U8);
311                    let (value, rem) = decode(bytes, &ty, types)?;
312                    let discriminant = types::parse_value::<u16>(&value)?;
313
314                    let variant = variants
315                        .iter()
316                        .find(|v| v.discriminant == discriminant)
317                        .ok_or(ArgsError::DecodingError("Variant not found".to_string()))?;
318                    bytes = rem;
319                    Ok((variant.name.clone(), bytes))
320                }
321            }
322        }
323        NamedCLType::List(inner) => {
324            let ty = Type(*inner.clone());
325            let mut bytes = bytes;
326            let mut decoded = "[".to_string();
327
328            let (len, rem) = types::from_bytes_or_err::<u32>(bytes)?;
329            bytes = rem;
330            for _ in 0..len {
331                let (value, rem) = decode(bytes, &ty, types)?;
332                bytes = rem;
333                decoded.push_str(format!("{},", value).as_str());
334            }
335            decoded.pop();
336            decoded.push(']');
337            match inner.as_ref() {
338                NamedCLType::Custom(_) => Ok((to_json(&decoded)?, bytes)),
339                _ => Ok((decoded, bytes))
340            }
341        }
342        _ => {
343            let result = types::from_bytes(&ty.0, bytes)?;
344            Ok(result)
345        }
346    }
347}
348
349fn to_json(str: &str) -> Result<String, ArgsError> {
350    let json =
351        Value::from_str(str).map_err(|_| ArgsError::DecodingError("Invalid JSON".to_string()))?;
352    serde_json::to_string_pretty(&json)
353        .map_err(|_| ArgsError::DecodingError("Invalid JSON".to_string()))
354}
355
356pub fn attached_value_arg() -> Arg {
357    Arg::new(ARG_ATTACHED_VALUE)
358        .help("The amount of CSPRs attached to the call")
359        .long(ARG_ATTACHED_VALUE)
360        .required(false)
361        .value_name(format!("{:?}", NamedCLType::U512))
362        .action(ArgAction::Set)
363}
364
365#[cfg(test)]
366mod tests {
367    use clap::{Arg, Command};
368    use odra::casper_types::{bytesrepr::Bytes, runtime_args};
369    use odra::schema::casper_contract_schema::{NamedCLType, Type};
370
371    use crate::test_utils::{self, NameMintInfo, PaymentInfo, PaymentVoucher};
372
373    const NAMED_TOKEN_METADATA_BYTES: [u8; 50] = [
374        4, 0, 0, 0, 107, 112, 111, 98, 0, 32, 74, 169, 209, 1, 0, 0, 1, 1, 226, 74, 54, 110, 186,
375        196, 135, 233, 243, 218, 49, 175, 91, 142, 42, 103, 172, 205, 97, 76, 95, 247, 61, 188, 60,
376        100, 10, 52, 124, 59, 94, 73
377    ];
378
379    const NAMED_TOKEN_METADATA_JSON: &str = r#"{
380  "token_hash": "kpob",
381  "expiration": "2000000000000",
382  "resolver": "Key::Hash(e24a366ebac487e9f3da31af5b8e2a67accd614c5ff73dbc3c640a347c3b5e49)"
383}"#;
384
385    #[test]
386    fn test_decode() {
387        let custom_types = test_utils::custom_types();
388
389        let ty = Type(NamedCLType::Custom("NameTokenMetadata".to_string()));
390        let (result, _bytes) =
391            super::decode(&NAMED_TOKEN_METADATA_BYTES, &ty, &custom_types).unwrap();
392        pretty_assertions::assert_eq!(result, NAMED_TOKEN_METADATA_JSON);
393    }
394
395    #[test]
396    fn test_command_args() {
397        let entry_point = test_utils::mock_entry_point();
398        let custom_types = test_utils::custom_types();
399
400        let args = entry_point
401            .arguments
402            .iter()
403            .flat_map(|arg| super::flat_arg(arg, &custom_types, false))
404            .flatten()
405            .collect::<Vec<_>>();
406
407        let expected = test_utils::mock_command_args();
408        pretty_assertions::assert_eq!(args, expected);
409    }
410
411    #[test]
412    fn test_compose() {
413        let entry_point = test_utils::mock_entry_point();
414
415        let mut cmd = Command::new("myprog");
416        test_utils::mock_command_args()
417            .into_iter()
418            .map(Into::into)
419            .for_each(|arg: Arg| {
420                cmd = cmd.clone().arg(arg);
421            });
422
423        let args = cmd.get_matches_from(vec![
424            "myprog",
425            "--voucher.payment.buyer",
426            "hash-56fef1f62d86ab68655c2a5d1c8b9ed8e60d5f7e59736e9d4c215a40b10f4a22",
427            "--voucher.payment.payment_id",
428            "id_001",
429            "--voucher.payment.amount",
430            "666",
431            "--voucher.names.label",
432            "kpob",
433            "--voucher.names.owner",
434            "hash-f01cec215ddfd4c4a19d58f9c917023391a1da871e047dc47a83ae55f6cfc20a",
435            "--voucher.names.token_expiration",
436            "1000000",
437            "--voucher.names.label",
438            "qwerty",
439            "--voucher.names.owner",
440            "hash-f01cec215ddfd4c4a19d58f9c917023391a1da871e047dc47a83ae55f6cfc20a",
441            "--voucher.names.token_expiration",
442            "1000000",
443            "--voucher.voucher_expiration",
444            "2000000",
445            "--signature",
446            "1,148,81,107,136,16,186,87,48,202,151",
447        ]);
448        let types = test_utils::custom_types();
449        let args = super::compose(&entry_point, &args, &types).unwrap();
450        let expected = runtime_args! {
451            "voucher" => PaymentVoucher::new(
452                PaymentInfo::new(
453                    "hash-56fef1f62d86ab68655c2a5d1c8b9ed8e60d5f7e59736e9d4c215a40b10f4a22",
454                    "id_001",
455                    "666"
456                ),
457                vec![
458                    NameMintInfo::new(
459                        "kpob",
460                        "hash-f01cec215ddfd4c4a19d58f9c917023391a1da871e047dc47a83ae55f6cfc20a",
461                        1000000
462                    ),
463                    NameMintInfo::new(
464                        "qwerty",
465                        "hash-f01cec215ddfd4c4a19d58f9c917023391a1da871e047dc47a83ae55f6cfc20a",
466                        1000000
467                    )
468                ],
469                2000000
470            ),
471            "signature" => Bytes::from(vec![1u8, 148u8, 81u8, 107u8, 136u8, 16u8, 186u8, 87u8, 48u8, 202u8, 151u8]),
472        };
473        pretty_assertions::assert_eq!(args, expected);
474    }
475}