Skip to content

Commit

Permalink
Merge branch 'main' into contract-alias-ls
Browse files Browse the repository at this point in the history
  • Loading branch information
fnando authored Aug 28, 2024
2 parents 2eb8630 + f7b007e commit b6777b6
Show file tree
Hide file tree
Showing 4 changed files with 270 additions and 243 deletions.
12 changes: 8 additions & 4 deletions cmd/crates/soroban-test/tests/it/help.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use soroban_cli::commands::contract;
use soroban_cli::commands::contract::{self, arg_parsing};
use soroban_test::TestEnv;

use crate::util::{invoke_custom as invoke, CUSTOM_TYPES, DEFAULT_CONTRACT_ID};
Expand Down Expand Up @@ -55,7 +55,7 @@ async fn complex_enum_help() {
async fn multi_arg_failure() {
assert!(matches!(
invoke_custom("multi_args", "--b").await.unwrap_err(),
contract::invoke::Error::MissingArgument(_)
contract::invoke::Error::ArgParsing(arg_parsing::Error::MissingArgument(_))
));
}

Expand All @@ -64,7 +64,9 @@ async fn handle_arg_larger_than_i32_failure() {
let res = invoke_custom("i32_", &format!("--i32_={}", u32::MAX)).await;
assert!(matches!(
res,
Err(contract::invoke::Error::CannotParseArg { .. })
Err(contract::invoke::Error::ArgParsing(
arg_parsing::Error::CannotParseArg { .. }
))
));
}

Expand All @@ -73,7 +75,9 @@ async fn handle_arg_larger_than_i64_failure() {
let res = invoke_custom("i64_", &format!("--i64_={}", u64::MAX)).await;
assert!(matches!(
res,
Err(contract::invoke::Error::CannotParseArg { .. })
Err(contract::invoke::Error::ArgParsing(
arg_parsing::Error::CannotParseArg { .. }
))
));
}

Expand Down
245 changes: 245 additions & 0 deletions cmd/soroban-cli/src/commands/contract/arg_parsing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
use std::collections::HashMap;
use std::convert::TryInto;
use std::ffi::OsString;
use std::fmt::Debug;
use std::path::PathBuf;

use clap::value_parser;
use ed25519_dalek::SigningKey;
use heck::ToKebabCase;

use soroban_env_host::xdr::{
self, Hash, InvokeContractArgs, ScAddress, ScSpecEntry, ScSpecFunctionV0, ScSpecTypeDef, ScVal,
ScVec,
};

use crate::commands::txn_result::TxnResult;
use crate::config::{self};
use soroban_spec_tools::Spec;

#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("parsing argument {arg}: {error}")]
CannotParseArg {
arg: String,
error: soroban_spec_tools::Error,
},
#[error("cannot print result {result:?}: {error}")]
CannotPrintResult {
result: ScVal,
error: soroban_spec_tools::Error,
},
#[error("function {0} was not found in the contract")]
FunctionNotFoundInContractSpec(String),
#[error("function name {0} is too long")]
FunctionNameTooLong(String),
#[error("argument count ({current}) surpasses maximum allowed count ({maximum})")]
MaxNumberOfArgumentsReached { current: usize, maximum: usize },
#[error(transparent)]
Xdr(#[from] xdr::Error),
#[error(transparent)]
StrVal(#[from] soroban_spec_tools::Error),
#[error("Missing argument {0}")]
MissingArgument(String),
#[error("")]
MissingFileArg(PathBuf),
}

pub fn build_host_function_parameters(
contract_id: &stellar_strkey::Contract,
slop: &[OsString],
spec_entries: &[ScSpecEntry],
config: &config::Args,
) -> Result<(String, Spec, InvokeContractArgs, Vec<SigningKey>), Error> {
let spec = Spec(Some(spec_entries.to_vec()));
let mut cmd = clap::Command::new(contract_id.to_string())
.no_binary_name(true)
.term_width(300)
.max_term_width(300);

for ScSpecFunctionV0 { name, .. } in spec.find_functions()? {
cmd = cmd.subcommand(build_custom_cmd(&name.to_utf8_string_lossy(), &spec)?);
}
cmd.build();
let long_help = cmd.render_long_help();
let mut matches_ = cmd.get_matches_from(slop);
let Some((function, matches_)) = &matches_.remove_subcommand() else {
println!("{long_help}");
std::process::exit(1);
};

let func = spec.find_function(function)?;
// create parsed_args in same order as the inputs to func
let mut signers: Vec<SigningKey> = vec![];
let parsed_args = func
.inputs
.iter()
.map(|i| {
let name = i.name.to_utf8_string()?;
if let Some(mut val) = matches_.get_raw(&name) {
let mut s = val.next().unwrap().to_string_lossy().to_string();
if matches!(i.type_, ScSpecTypeDef::Address) {
let cmd = crate::commands::keys::address::Cmd {
name: s.clone(),
hd_path: Some(0),
locator: config.locator.clone(),
};
if let Ok(address) = cmd.public_key() {
s = address.to_string();
}
if let Ok(key) = cmd.private_key() {
signers.push(key);
}
}
spec.from_string(&s, &i.type_)
.map_err(|error| Error::CannotParseArg { arg: name, error })
} else if matches!(i.type_, ScSpecTypeDef::Option(_)) {
Ok(ScVal::Void)
} else if let Some(arg_path) = matches_.get_one::<PathBuf>(&fmt_arg_file_name(&name)) {
if matches!(i.type_, ScSpecTypeDef::Bytes | ScSpecTypeDef::BytesN(_)) {
Ok(ScVal::try_from(
&std::fs::read(arg_path)
.map_err(|_| Error::MissingFileArg(arg_path.clone()))?,
)
.map_err(|()| Error::CannotParseArg {
arg: name.clone(),
error: soroban_spec_tools::Error::Unknown,
})?)
} else {
let file_contents = std::fs::read_to_string(arg_path)
.map_err(|_| Error::MissingFileArg(arg_path.clone()))?;
tracing::debug!(
"file {arg_path:?}, has contents:\n{file_contents}\nAnd type {:#?}\n{}",
i.type_,
file_contents.len()
);
spec.from_string(&file_contents, &i.type_)
.map_err(|error| Error::CannotParseArg { arg: name, error })
}
} else {
Err(Error::MissingArgument(name))
}
})
.collect::<Result<Vec<_>, Error>>()?;

let contract_address_arg = ScAddress::Contract(Hash(contract_id.0));
let function_symbol_arg = function
.try_into()
.map_err(|()| Error::FunctionNameTooLong(function.clone()))?;

let final_args =
parsed_args
.clone()
.try_into()
.map_err(|_| Error::MaxNumberOfArgumentsReached {
current: parsed_args.len(),
maximum: ScVec::default().max_len(),
})?;

let invoke_args = InvokeContractArgs {
contract_address: contract_address_arg,
function_name: function_symbol_arg,
args: final_args,
};

Ok((function.clone(), spec, invoke_args, signers))
}

fn build_custom_cmd(name: &str, spec: &Spec) -> Result<clap::Command, Error> {
let func = spec
.find_function(name)
.map_err(|_| Error::FunctionNotFoundInContractSpec(name.to_string()))?;

// Parse the function arguments
let inputs_map = &func
.inputs
.iter()
.map(|i| (i.name.to_utf8_string().unwrap(), i.type_.clone()))
.collect::<HashMap<String, ScSpecTypeDef>>();
let name: &'static str = Box::leak(name.to_string().into_boxed_str());
let mut cmd = clap::Command::new(name)
.no_binary_name(true)
.term_width(300)
.max_term_width(300);
let kebab_name = name.to_kebab_case();
if kebab_name != name {
cmd = cmd.alias(kebab_name);
}
let doc: &'static str = Box::leak(func.doc.to_utf8_string_lossy().into_boxed_str());
let long_doc: &'static str = Box::leak(arg_file_help(doc).into_boxed_str());

cmd = cmd.about(Some(doc)).long_about(long_doc);
for (name, type_) in inputs_map {
let mut arg = clap::Arg::new(name);
let file_arg_name = fmt_arg_file_name(name);
let mut file_arg = clap::Arg::new(&file_arg_name);
arg = arg
.long(name)
.alias(name.to_kebab_case())
.num_args(1)
.value_parser(clap::builder::NonEmptyStringValueParser::new())
.long_help(spec.doc(name, type_)?);

file_arg = file_arg
.long(&file_arg_name)
.alias(file_arg_name.to_kebab_case())
.num_args(1)
.hide(true)
.value_parser(value_parser!(PathBuf))
.conflicts_with(name);

if let Some(value_name) = spec.arg_value_name(type_, 0) {
let value_name: &'static str = Box::leak(value_name.into_boxed_str());
arg = arg.value_name(value_name);
}

// Set up special-case arg rules
arg = match type_ {
ScSpecTypeDef::Bool => arg
.num_args(0..1)
.default_missing_value("true")
.default_value("false")
.num_args(0..=1),
ScSpecTypeDef::Option(_val) => arg.required(false),
ScSpecTypeDef::I256 | ScSpecTypeDef::I128 | ScSpecTypeDef::I64 | ScSpecTypeDef::I32 => {
arg.allow_hyphen_values(true)
}
_ => arg,
};

cmd = cmd.arg(arg);
cmd = cmd.arg(file_arg);
}
Ok(cmd)
}

fn fmt_arg_file_name(name: &str) -> String {
format!("{name}-file-path")
}

fn arg_file_help(docs: &str) -> String {
format!(
r#"{docs}
Usage Notes:
Each arg has a corresponding --<arg_name>-file-path which is a path to a file containing the corresponding JSON argument.
Note: The only types which aren't JSON are Bytes and BytesN, which are raw bytes"#
)
}

pub fn output_to_string(
spec: &Spec,
res: &ScVal,
function: &str,
) -> Result<TxnResult<String>, Error> {
let mut res_str = String::new();
if let Some(output) = spec.find_function(function)?.outputs.first() {
res_str = spec
.xdr_to_json(res, output)
.map_err(|e| Error::CannotPrintResult {
result: res.clone(),
error: e,
})?
.to_string();
}
Ok(TxnResult::Res(res_str))
}
Loading

0 comments on commit b6777b6

Please sign in to comment.