use std::{collections::HashMap, ffi::OsString, path::PathBuf};
use clap::{value_parser, Parser};
use ed25519_dalek::SigningKey;
use heck::ToKebabCase;
use loam_sdk::soroban_sdk::xdr::{
self, AccountId, HostFunction, InvokeContractArgs, InvokeHostFunctionOp, Memo, MuxedAccount,
Operation, OperationBody, Preconditions, ScSpecEntry, ScSpecFunctionV0, ScSpecTypeDef,
ScString, ScSymbol, ScVal, SequenceNumber, Transaction, TransactionExt, TransactionMeta,
TransactionMetaV3, Uint256, VecM,
};
use soroban_cli::{
commands::{self, config, contract::invoke},
fee, rpc,
};
use soroban_spec_tools::Spec;
use crate::testnet::{self, contract_address, invoke_smartdeploy};
#[derive(Parser, Debug, Clone)]
pub struct Cmd {
#[arg(long, visible_alias = "deploy-as")]
pub deployed_name: String,
#[arg(long)]
pub published_name: String,
#[arg(last = true, id = "CONTRACT_FN_AND_ARGS")]
pub slop: Vec<OsString>,
#[command(flatten)]
pub config: config::Args,
#[command(flatten)]
pub fee: fee::Args,
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error(transparent)]
Invoke(#[from] invoke::Error),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
SmartdeployBuild(#[from] smartdeploy_build::Error),
#[error(transparent)]
Install(#[from] super::install::Error),
#[error(transparent)]
Rpc(#[from] rpc::Error),
#[error(transparent)]
SpecTools(#[from] soroban_spec_tools::Error),
#[error(transparent)]
Config(#[from] config::Error),
#[error(transparent)]
Xdr(#[from] xdr::Error),
#[error("Cannot parse contract spec")]
CannotParseContractSpec,
#[error("argument count ({current}) surpasses maximum allowed count ({maximum})")]
MaxNumberOfArgumentsReached { current: usize, maximum: usize },
#[error("function {0} was not found in the contract")]
FunctionNotFoundInContractSpec(String),
#[error("parsing argument {arg}: {error}")]
CannotParseArg {
arg: String,
error: soroban_spec_tools::Error,
},
#[error("function name {0} is too long")]
FunctionNameTooLong(String),
#[error("Missing file argument {0:#?}")]
MissingFileArg(PathBuf),
#[error("Missing argument {0}")]
MissingArgument(String),
}
impl Cmd {
pub async fn run(&self) -> Result<(), Error> {
self.invoke().await?;
Ok(())
}
pub async fn hash(&self) -> Result<xdr::Hash, Error> {
let res =
invoke_smartdeploy(&["fetch_hash", "--contract_name", &self.published_name]).await?;
let res = res.trim_matches('"');
Ok(res.parse().unwrap())
}
pub async fn wasm(&self) -> Result<Vec<u8>, Error> {
Ok(testnet::client()?
.get_remote_wasm_from_hash(self.hash().await?)
.await?)
}
pub async fn spec_entries(&self) -> Result<Vec<ScSpecEntry>, Error> {
soroban_spec::read::from_wasm(&self.wasm().await?)
.map_err(|_| Error::CannotParseContractSpec)
}
async fn invoke(&self) -> Result<(), Error> {
let client = testnet::client()?;
let key = self.config.key_pair()?;
let public_strkey =
stellar_strkey::ed25519::PublicKey(key.verifying_key().to_bytes()).to_string();
let contract_address = contract_address();
let args = if self.slop.is_empty() {
ScVal::Void
} else {
let (function_symbol_arg, final_args) = build_host_function_parameters(
&self.deployed_name,
&self.spec_entries().await?,
&self.slop,
)?;
ScVal::Vec(Some(
vec![
ScVal::Symbol(function_symbol_arg),
ScVal::Vec(Some(final_args.try_into().unwrap())),
]
.try_into()
.unwrap(),
))
};
let invoke_contract_args = InvokeContractArgs {
contract_address: contract_address.clone(),
function_name: "deploy".try_into().unwrap(),
args: [
ScVal::String(ScString(self.published_name.clone().try_into().unwrap())),
ScVal::Void,
ScVal::String(ScString(self.deployed_name.clone().try_into().unwrap())),
ScVal::Address(xdr::ScAddress::Account(AccountId(
xdr::PublicKey::PublicKeyTypeEd25519(Uint256(key.verifying_key().to_bytes())),
))),
ScVal::Void,
args,
]
.try_into()
.unwrap(),
};
let account_details = client.get_account(&public_strkey).await?;
let sequence: i64 = account_details.seq_num.into();
let tx = build_invoke_contract_tx(invoke_contract_args, sequence + 1, self.fee.fee, &key)?;
let (
_,
TransactionMeta::V3(TransactionMetaV3 {
soroban_meta: Some(xdr::SorobanTransactionMeta { return_value, .. }),
..
}),
_,
) = client
.prepare_and_send_transaction(
&tx,
&key,
&[],
&testnet::network_passphrase(),
None,
None,
)
.await?
else {
panic!("AAH");
};
println!("{return_value:#?}");
Ok(())
}
}
fn build_host_function_parameters(
name: &str,
spec_entries: &[ScSpecEntry],
slop: &[OsString],
) -> Result<(ScSymbol, Vec<ScVal>), Error> {
let spec = soroban_spec_tools::Spec(Some(spec_entries.to_vec()));
let mut cmd = clap::Command::new(name.to_owned())
.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 mut matches_ = cmd.get_matches_from(slop);
let (function, matches_) = &matches_.remove_subcommand().unwrap();
let func = spec.find_function(function)?;
let parsed_args = func
.inputs
.iter()
.map(|i| {
let name = i.name.to_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 = commands::config::identity::address::Cmd {
name: Some(s.clone()),
hd_path: Some(0),
locator: config::locator::Args::default(),
};
if let Ok(address) = cmd.public_key() {
s = address.to_string();
}
}
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()))?,
)
.unwrap())
} else {
let file_contents = std::fs::read_to_string(arg_path)
.map_err(|_| Error::MissingFileArg(arg_path.clone()))?;
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 function_symbol_arg = function
.try_into()
.map_err(|_| Error::FunctionNameTooLong(function.clone()))?;
Ok((function_symbol_arg, parsed_args))
}
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_string(), 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 func = spec.find_function(name).unwrap();
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_).unwrap());
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_ {
xdr::ScSpecTypeDef::Bool => arg
.num_args(0..1)
.default_missing_value("true")
.default_value("false")
.num_args(0..=1),
xdr::ScSpecTypeDef::Option(_val) => arg.required(false),
xdr::ScSpecTypeDef::I256
| xdr::ScSpecTypeDef::I128
| xdr::ScSpecTypeDef::I64
| xdr::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 Bytes which are raw bytes"#
)
}
fn build_invoke_contract_tx(
parameters: InvokeContractArgs,
sequence: i64,
fee: u32,
key: &SigningKey,
) -> Result<Transaction, Error> {
let op = Operation {
source_account: None,
body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
host_function: HostFunction::InvokeContract(parameters),
auth: VecM::default(),
}),
};
Ok(Transaction {
source_account: MuxedAccount::Ed25519(Uint256(key.verifying_key().to_bytes())),
fee,
seq_num: SequenceNumber(sequence),
cond: Preconditions::None,
memo: Memo::None,
operations: vec![op].try_into()?,
ext: TransactionExt::V0,
})
}