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#[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 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}