use std::{
collections::HashMap,
path::{Path, PathBuf}
};
use anyhow::Result;
use clap::ArgMatches;
use odra::{
contract_def::HasIdent,
entry_point_callback::EntryPointsCaller,
host::{EntryPointsCallerProvider, HostEnv},
schema::{SchemaCustomTypes, SchemaEntrypoints, SchemaEvents},
OdraContract
};
use crate::{
cmd::{
CompletionsCmd, ConfigCmd, ContractsCmd, DeployCmd, DeployScript, InspectCmd, MainCmd,
MutableCommand, OdraCommand, PrintEventsCmd, Scenario, ScenarioMetadata, ScenariosCmd,
StatusCmd, TransferCmd, WhoamiCmd, COMPLETIONS_SUBCOMMAND, CONFIG_SUBCOMMAND,
CONTRACTS_SUBCOMMAND, DEPLOY_SUBCOMMAND, INSPECT_SUBCOMMAND, PRINT_EVENTS_SUBCOMMAND,
REPL_SUBCOMMAND, SCENARIOS_SUBCOMMAND, STATUS_SUBCOMMAND, TRANSFER_SUBCOMMAND,
WHOAMI_SUBCOMMAND
},
container::FileContractStorage,
custom_types::CustomTypes,
utils::get_default_contracts_file,
ContractProvider, DeployedContractsContainer
};
mod completer;
mod env_setup;
mod repl;
pub struct OdraCli {
main_cmd: MainCmd,
deploy_cmd: Option<DeployCmd>,
contracts_cmd: ContractsCmd,
print_events_cmd: PrintEventsCmd,
scenarios_cmd: ScenariosCmd,
whoami_cmd: WhoamiCmd,
status_cmd: StatusCmd,
inspect_cmd: InspectCmd,
config_cmd: ConfigCmd,
transfer_cmd: TransferCmd,
completions_cmd: CompletionsCmd,
custom_types: CustomTypes,
host_env: HostEnv,
callers: HashMap<(String, String), EntryPointsCaller>,
default_contract_path: Option<PathBuf>
}
impl Default for OdraCli {
fn default() -> Self {
Self::new()
}
}
impl OdraCli {
pub fn new() -> Self {
let host_env = env_setup::create_host_env();
Self {
main_cmd: MainCmd::default(),
deploy_cmd: None,
contracts_cmd: ContractsCmd::default(),
print_events_cmd: PrintEventsCmd::default(),
scenarios_cmd: ScenariosCmd::default(),
whoami_cmd: WhoamiCmd::new(),
status_cmd: StatusCmd::default(),
inspect_cmd: InspectCmd::default(),
config_cmd: ConfigCmd,
transfer_cmd: TransferCmd,
completions_cmd: CompletionsCmd,
host_env,
custom_types: CustomTypes::default(),
callers: HashMap::default(),
default_contract_path: None
}
}
pub fn about(mut self, about: &'static str) -> Self {
self.main_cmd = self.main_cmd.about(about);
self
}
pub fn contracts_file<P: AsRef<Path>>(mut self, path: P) -> Self {
self.default_contract_path = Some(path.as_ref().to_path_buf());
self
}
pub fn contract<T: SchemaEntrypoints + SchemaCustomTypes + SchemaEvents + OdraContract>(
mut self
) -> Self {
self.callers.insert(
(T::HostRef::ident(), T::HostRef::ident()),
T::HostRef::entry_points_caller(&self.host_env)
);
self.custom_types.register::<T>();
self.contracts_cmd.add_contract::<T>();
self.print_events_cmd.add_contract::<T>();
self.status_cmd.add_contract::<T>();
self.inspect_cmd.add_contract::<T>();
self
}
pub fn named_contract<
T: SchemaEntrypoints + SchemaCustomTypes + SchemaEvents + OdraContract
>(
mut self,
name: String
) -> Self {
self.callers.insert(
(T::HostRef::ident(), name.clone()),
T::HostRef::entry_points_caller(&self.host_env)
);
self.custom_types.register::<T>();
self.contracts_cmd.add_contract_named::<T>(name.clone());
self.print_events_cmd.add_contract_named::<T>(name.clone());
self.status_cmd.add_contract_named::<T>(name.clone());
self.inspect_cmd.add_contract_named::<T>(name);
self
}
pub fn deploy(mut self, script: impl DeployScript + 'static) -> Self {
let cmd = DeployCmd::new(script);
self.main_cmd = self.main_cmd.subcommand(&cmd);
self.deploy_cmd = Some(cmd);
self
}
pub fn scenario<S: ScenarioMetadata + Scenario>(mut self, scenario: S) -> Self {
self.scenarios_cmd.add_scenario(scenario);
self
}
pub fn build(mut self) -> Self {
self.main_cmd = self.main_cmd.subcommand(&self.contracts_cmd);
self.main_cmd = self.main_cmd.subcommand(&self.scenarios_cmd);
self.main_cmd = self.main_cmd.subcommand(&self.print_events_cmd);
self.main_cmd = self.main_cmd.subcommand(&self.whoami_cmd);
self.main_cmd = self.main_cmd.subcommand(&self.status_cmd);
self.main_cmd = self.main_cmd.subcommand(&self.inspect_cmd);
self.main_cmd = self.main_cmd.subcommand(&self.config_cmd);
self.main_cmd = self.main_cmd.subcommand(&self.transfer_cmd);
self.main_cmd = self.main_cmd.subcommand(&self.completions_cmd);
self.main_cmd = self.main_cmd.subcommand(
clap::Command::new(REPL_SUBCOMMAND)
.about("Starts an interactive REPL session, keeping the host environment warm across commands.")
);
self
}
pub fn run(self) {
let (cmd, args, contracts_path) = self.main_cmd.get_matches();
let contracts_path = match contracts_path {
Some(path) => Some(path),
None => self.default_contract_path.clone()
};
let mut container = self
.load_container(contracts_path.clone())
.unwrap_or_else(|e| {
prettycli::error(&format!("{e:#}"));
std::process::exit(1);
});
if let Err(err) = self.register_deployed_contracts(&container, &contracts_path) {
prettycli::error(&format!("{err:#}"));
std::process::exit(1);
}
if let Err(err) = self.dispatch(&cmd, &args, &mut container) {
prettycli::error(&format!("{err:#}"));
std::process::exit(1);
}
}
fn load_container(
&self,
contracts_path: Option<PathBuf>
) -> Result<DeployedContractsContainer> {
let storage = FileContractStorage::new(contracts_path)
.map_err(|e| anyhow::anyhow!("Failed to create contract storage: {e}"))?;
Ok(DeployedContractsContainer::instance(storage))
}
fn register_deployed_contracts(
&self,
container: &DeployedContractsContainer,
contracts_path: &Option<PathBuf>
) -> Result<()> {
for deployed_contract in container.all_contracts() {
let caller = self
.callers
.get(&(deployed_contract.name(), deployed_contract.key_name()))
.ok_or_else(|| {
let path = match contracts_path {
Some(path) => path.to_str().map(|s| s.to_string()).unwrap_or_default(),
None => get_default_contracts_file()
};
anyhow::anyhow!(
"Caller for `{}` not found. The contract is registered in '{}' file, but not in the CLI builder. Make sure you have added it to the builder using `.contract::<{}>()`.",
deployed_contract.key_name(), path, deployed_contract.name()
)
})?
.clone();
self.host_env.register_contract(
deployed_contract.address(),
deployed_contract.key_name(),
caller
);
}
Ok(())
}
fn dispatch(
&self,
cmd: &str,
args: &ArgMatches,
container: &mut DeployedContractsContainer
) -> Result<()> {
match cmd {
DEPLOY_SUBCOMMAND => self
.deploy_cmd
.as_ref()
.ok_or_else(|| {
anyhow::anyhow!("Deploy command not found. Did you forget to add it?")
})?
.run(&self.host_env, args, &self.custom_types, container),
CONTRACTS_SUBCOMMAND => self.run_command(&self.contracts_cmd, args, container),
PRINT_EVENTS_SUBCOMMAND => self.run_command(&self.print_events_cmd, args, container),
SCENARIOS_SUBCOMMAND => self.run_command(&self.scenarios_cmd, args, container),
WHOAMI_SUBCOMMAND => self.run_command(&self.whoami_cmd, args, container),
STATUS_SUBCOMMAND => self.run_command(&self.status_cmd, args, container),
INSPECT_SUBCOMMAND => self.run_command(&self.inspect_cmd, args, container),
CONFIG_SUBCOMMAND => self.run_command(&self.config_cmd, args, container),
TRANSFER_SUBCOMMAND => self.run_command(&self.transfer_cmd, args, container),
COMPLETIONS_SUBCOMMAND => self
.completions_cmd
.generate(args, self.main_cmd.to_command(&[])),
REPL_SUBCOMMAND => repl::run(self, container),
_ => Err(anyhow::anyhow!("Unknown command: {cmd}"))
}
}
fn run_command<T: OdraCommand>(
&self,
cmd: &T,
args: &ArgMatches,
container: &DeployedContractsContainer
) -> Result<()> {
cmd.run(&self.host_env, args, &self.custom_types, container)
}
}