1#![feature(box_patterns, error_generic_member_access)]
7use std::collections::BTreeSet;
8
9use clap::{command, Arg, Command};
10use cmd::{OdraCliCommand, OdraCommand};
11use deploy::DeployScript;
12use odra::schema::{casper_contract_schema::CustomType, SchemaCustomTypes, SchemaEntrypoints};
13use odra::{
14 contract_def::HasIdent,
15 host::{EntryPointsCallerProvider, HostEnv},
16 OdraContract
17};
18
19mod args;
20mod cmd;
21mod container;
22mod entry_point;
23#[cfg(test)]
24mod test_utils;
25mod types;
26
27pub use args::CommandArg;
28pub use cmd::scenario::{ScenarioArgs, ScenarioError};
29pub use container::DeployedContractsContainer;
30use scenario::{Scenario, ScenarioMetadata};
31
32const CONTRACTS_SUBCOMMAND: &str = "contract";
33const SCENARIOS_SUBCOMMAND: &str = "scenario";
34const DEPLOY_SUBCOMMAND: &str = "deploy";
35
36pub(crate) type CustomTypeSet = BTreeSet<CustomType>;
37
38pub mod scenario {
39 pub use crate::cmd::scenario::{
45 Scenario, ScenarioArgs as Args, ScenarioError as Error, ScenarioMetadata
46 };
47}
48
49pub mod deploy {
50 pub use crate::cmd::deploy::{DeployError as Error, DeployScript};
55}
56
57pub struct OdraCli {
59 main_cmd: Command,
60 scenarios_cmd: Command,
61 contracts_cmd: Command,
62 commands: Vec<OdraCliCommand>,
63 custom_types: CustomTypeSet,
64 host_env: HostEnv
65}
66
67impl Default for OdraCli {
68 fn default() -> Self {
69 Self::new()
70 }
71}
72
73impl OdraCli {
74 pub fn new() -> Self {
76 let contracts_cmd = Command::new(CONTRACTS_SUBCOMMAND)
77 .about("Commands for interacting with contracts")
78 .subcommand_required(true)
79 .arg_required_else_help(true);
80 let scenarios_cmd = Command::new(SCENARIOS_SUBCOMMAND)
81 .about("Commands for running user-defined scenarios")
82 .subcommand_required(true)
83 .arg_required_else_help(true);
84 let main_cmd = Command::new("Odra CLI")
85 .subcommand_required(true)
86 .arg_required_else_help(true);
87
88 Self {
89 main_cmd,
90 commands: vec![],
91 custom_types: CustomTypeSet::new(),
92 host_env: odra_casper_livenet_env::env(),
93 contracts_cmd,
94 scenarios_cmd
95 }
96 }
97
98 pub fn about(mut self, about: &str) -> Self {
100 self.main_cmd = self.main_cmd.about(about.to_string());
101 self
102 }
103
104 pub fn contract<T: SchemaEntrypoints + SchemaCustomTypes + OdraContract>(mut self) -> Self {
109 let contract_name = T::HostRef::ident();
110 if let Ok(container) = DeployedContractsContainer::load() {
111 let caller = T::HostRef::entry_points_caller(&self.host_env);
112 let address = container
113 .address(&contract_name)
114 .expect("Contract not found");
115 self.host_env
116 .register_contract(address, contract_name.clone(), caller);
117 }
118 self.custom_types
119 .extend(T::schema_types().into_iter().flatten());
120
121 let mut contract_cmd = Command::new(&contract_name)
123 .about(format!(
124 "Commands for interacting with the {} contract",
125 &contract_name
126 ))
127 .subcommand_required(true)
128 .arg_required_else_help(true);
129 for entry_point in T::schema_entrypoints() {
130 if entry_point.name == "init" {
131 continue;
132 }
133 let mut ep_cmd = Command::new(&entry_point.name)
134 .about(entry_point.description.clone().unwrap_or_default());
135 for arg in args::entry_point_args(&entry_point, &self.custom_types) {
136 ep_cmd = ep_cmd.arg(arg);
137 }
138 ep_cmd = ep_cmd.arg(args::attached_value_arg());
139 contract_cmd = contract_cmd.subcommand(ep_cmd);
140 }
141 self.contracts_cmd = self.contracts_cmd.subcommand(contract_cmd);
142
143 self.commands
145 .push(OdraCliCommand::new_contract::<T>(contract_name));
146 self
147 }
148
149 pub fn deploy(mut self, script: impl DeployScript + 'static) -> Self {
153 self.main_cmd = self
155 .main_cmd
156 .subcommand(command!(DEPLOY_SUBCOMMAND).about("Runs the deploy script"));
157 self.commands.push(OdraCliCommand::new_deploy(script));
159 self
160 }
161
162 pub fn scenario<S: ScenarioMetadata + Scenario>(mut self, scenario: S) -> Self {
168 let mut scenario_cmd = Command::new(S::NAME).about(S::DESCRIPTION);
170 let args = scenario
171 .args()
172 .into_iter()
173 .map(Into::into)
174 .collect::<Vec<Arg>>();
175 for arg in args {
176 scenario_cmd = scenario_cmd.arg(arg);
177 }
178
179 self.scenarios_cmd = self.scenarios_cmd.subcommand(scenario_cmd);
180
181 self.commands.push(OdraCliCommand::new_scenario(scenario));
183 self
184 }
185
186 pub fn build(mut self) -> Self {
188 self.main_cmd = self.main_cmd.subcommand(self.contracts_cmd.clone());
189 self.main_cmd = self.main_cmd.subcommand(self.scenarios_cmd.clone());
190 self
191 }
192
193 pub fn run(self) {
195 let matches = self.main_cmd.get_matches();
196 let (cmd, args) = matches
197 .subcommand()
198 .and_then(|(subcommand, sub_matches)| match subcommand {
199 DEPLOY_SUBCOMMAND => {
200 find_deploy(&self.commands).map(|deploy| (deploy, sub_matches))
201 }
202 CONTRACTS_SUBCOMMAND => {
203 sub_matches
204 .subcommand()
205 .map(|(contract_name, entrypoint_matches)| {
206 (
207 find_contract(&self.commands, contract_name),
208 entrypoint_matches
209 )
210 })
211 }
212 SCENARIOS_SUBCOMMAND => {
213 sub_matches.subcommand().map(|(subcommand, sub_matches)| {
214 (find_scenario(&self.commands, subcommand), sub_matches)
215 })
216 }
217 _ => unreachable!()
218 })
219 .expect("Subcommand not found");
220
221 match cmd.run(&self.host_env, args, &self.custom_types) {
222 Ok(_) => prettycli::info("Command executed successfully"),
223 Err(err) => prettycli::error(&format!("{:?}", err))
224 }
225 }
226}
227
228fn find_scenario<'a>(commands: &'a [OdraCliCommand], name: &str) -> &'a OdraCliCommand {
229 commands
230 .iter()
231 .find(|cmd| match cmd {
232 OdraCliCommand::Scenario(scenario) => scenario.name() == name,
233 _ => false
234 })
235 .unwrap()
236}
237
238fn find_deploy(commands: &[OdraCliCommand]) -> Option<&OdraCliCommand> {
239 commands
240 .iter()
241 .find(|cmd| matches!(cmd, OdraCliCommand::Deploy(_)))
242}
243
244fn find_contract<'a>(commands: &'a [OdraCliCommand], contract_name: &str) -> &'a OdraCliCommand {
245 commands
246 .iter()
247 .find(|cmd| match cmd {
248 OdraCliCommand::Contract(contract) => contract.name() == contract_name,
249 _ => false
250 })
251 .unwrap()
252}