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 crate::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)?;
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()))?;
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);
}
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))
}