1use std::{
2 collections::HashMap,
3 path::{Path, PathBuf}
4};
5
6use anyhow::Result;
7use clap::ArgMatches;
8use odra::{
9 contract_def::HasIdent,
10 entry_point_callback::EntryPointsCaller,
11 host::{EntryPointsCallerProvider, HostEnv},
12 schema::{SchemaCustomTypes, SchemaEntrypoints, SchemaEvents},
13 OdraContract
14};
15
16use crate::{
17 cmd::{
18 CompletionsCmd, ConfigCmd, ContractsCmd, DeployCmd, DeployScript, InspectCmd, MainCmd,
19 MutableCommand, OdraCommand, PrintEventsCmd, Scenario, ScenarioMetadata, ScenariosCmd,
20 StatusCmd, TransferCmd, WhoamiCmd, COMPLETIONS_SUBCOMMAND, CONFIG_SUBCOMMAND,
21 CONTRACTS_SUBCOMMAND, DEPLOY_SUBCOMMAND, INSPECT_SUBCOMMAND, PRINT_EVENTS_SUBCOMMAND,
22 REPL_SUBCOMMAND, SCENARIOS_SUBCOMMAND, STATUS_SUBCOMMAND, TRANSFER_SUBCOMMAND,
23 WHOAMI_SUBCOMMAND
24 },
25 container::FileContractStorage,
26 custom_types::CustomTypes,
27 utils::get_default_contracts_file,
28 ContractProvider, DeployedContractsContainer
29};
30
31mod completer;
32mod env_setup;
33mod repl;
34
35pub struct OdraCli {
37 main_cmd: MainCmd,
38 deploy_cmd: Option<DeployCmd>,
39 contracts_cmd: ContractsCmd,
40 print_events_cmd: PrintEventsCmd,
41 scenarios_cmd: ScenariosCmd,
42 whoami_cmd: WhoamiCmd,
43 status_cmd: StatusCmd,
44 inspect_cmd: InspectCmd,
45 config_cmd: ConfigCmd,
46 transfer_cmd: TransferCmd,
47 completions_cmd: CompletionsCmd,
48 custom_types: CustomTypes,
49 host_env: HostEnv,
50 callers: HashMap<(String, String), EntryPointsCaller>,
51 default_contract_path: Option<PathBuf>
52}
53
54impl Default for OdraCli {
55 fn default() -> Self {
56 Self::new()
57 }
58}
59
60impl OdraCli {
61 pub fn new() -> Self {
63 let host_env = env_setup::create_host_env();
64 Self {
65 main_cmd: MainCmd::default(),
66 deploy_cmd: None,
67 contracts_cmd: ContractsCmd::default(),
68 print_events_cmd: PrintEventsCmd::default(),
69 scenarios_cmd: ScenariosCmd::default(),
70 whoami_cmd: WhoamiCmd::new(),
71 status_cmd: StatusCmd::default(),
72 inspect_cmd: InspectCmd::default(),
73 config_cmd: ConfigCmd,
74 transfer_cmd: TransferCmd,
75 completions_cmd: CompletionsCmd,
76 host_env,
77 custom_types: CustomTypes::default(),
78 callers: HashMap::default(),
79 default_contract_path: None
80 }
81 }
82
83 pub fn about(mut self, about: &'static str) -> Self {
85 self.main_cmd = self.main_cmd.about(about);
86 self
87 }
88
89 pub fn contracts_file<P: AsRef<Path>>(mut self, path: P) -> Self {
93 self.default_contract_path = Some(path.as_ref().to_path_buf());
94 self
95 }
96
97 pub fn contract<T: SchemaEntrypoints + SchemaCustomTypes + SchemaEvents + OdraContract>(
102 mut self
103 ) -> Self {
104 self.callers.insert(
105 (T::HostRef::ident(), T::HostRef::ident()),
106 T::HostRef::entry_points_caller(&self.host_env)
107 );
108 self.custom_types.register::<T>();
109 self.contracts_cmd.add_contract::<T>();
110 self.print_events_cmd.add_contract::<T>();
111 self.status_cmd.add_contract::<T>();
112 self.inspect_cmd.add_contract::<T>();
113 self
114 }
115
116 pub fn named_contract<
121 T: SchemaEntrypoints + SchemaCustomTypes + SchemaEvents + OdraContract
122 >(
123 mut self,
124 name: String
125 ) -> Self {
126 self.callers.insert(
127 (T::HostRef::ident(), name.clone()),
128 T::HostRef::entry_points_caller(&self.host_env)
129 );
130 self.custom_types.register::<T>();
131 self.contracts_cmd.add_contract_named::<T>(name.clone());
132 self.print_events_cmd.add_contract_named::<T>(name.clone());
133 self.status_cmd.add_contract_named::<T>(name.clone());
134 self.inspect_cmd.add_contract_named::<T>(name);
135 self
136 }
137
138 pub fn deploy(mut self, script: impl DeployScript + 'static) -> Self {
142 let cmd = DeployCmd::new(script);
143 self.main_cmd = self.main_cmd.subcommand(&cmd);
144 self.deploy_cmd = Some(cmd);
145 self
146 }
147
148 pub fn scenario<S: ScenarioMetadata + Scenario>(mut self, scenario: S) -> Self {
154 self.scenarios_cmd.add_scenario(scenario);
155 self
156 }
157
158 pub fn build(mut self) -> Self {
160 self.main_cmd = self.main_cmd.subcommand(&self.contracts_cmd);
161 self.main_cmd = self.main_cmd.subcommand(&self.scenarios_cmd);
162 self.main_cmd = self.main_cmd.subcommand(&self.print_events_cmd);
163 self.main_cmd = self.main_cmd.subcommand(&self.whoami_cmd);
164 self.main_cmd = self.main_cmd.subcommand(&self.status_cmd);
165 self.main_cmd = self.main_cmd.subcommand(&self.inspect_cmd);
166 self.main_cmd = self.main_cmd.subcommand(&self.config_cmd);
167 self.main_cmd = self.main_cmd.subcommand(&self.transfer_cmd);
168 self.main_cmd = self.main_cmd.subcommand(&self.completions_cmd);
169 self.main_cmd = self.main_cmd.subcommand(
170 clap::Command::new(REPL_SUBCOMMAND)
171 .about("Starts an interactive REPL session, keeping the host environment warm across commands.")
172 );
173 self
174 }
175
176 pub fn run(self) {
178 let (cmd, args, contracts_path) = self.main_cmd.get_matches();
179 let contracts_path = match contracts_path {
180 Some(path) => Some(path),
181 None => self.default_contract_path.clone()
182 };
183
184 let mut container = self
186 .load_container(contracts_path.clone())
187 .unwrap_or_else(|e| {
188 prettycli::error(&format!("{e:#}"));
189 std::process::exit(1);
190 });
191
192 if let Err(err) = self.register_deployed_contracts(&container, &contracts_path) {
194 prettycli::error(&format!("{err:#}"));
195 std::process::exit(1);
196 }
197
198 if let Err(err) = self.dispatch(&cmd, &args, &mut container) {
199 prettycli::error(&format!("{err:#}"));
200 std::process::exit(1);
201 }
202 }
203
204 fn load_container(
206 &self,
207 contracts_path: Option<PathBuf>
208 ) -> Result<DeployedContractsContainer> {
209 let storage = FileContractStorage::new(contracts_path)
210 .map_err(|e| anyhow::anyhow!("Failed to create contract storage: {e}"))?;
211 Ok(DeployedContractsContainer::instance(storage))
212 }
213
214 fn register_deployed_contracts(
220 &self,
221 container: &DeployedContractsContainer,
222 contracts_path: &Option<PathBuf>
223 ) -> Result<()> {
224 for deployed_contract in container.all_contracts() {
225 let caller = self
226 .callers
227 .get(&(deployed_contract.name(), deployed_contract.key_name()))
228 .ok_or_else(|| {
229 let path = match contracts_path {
230 Some(path) => path.to_str().map(|s| s.to_string()).unwrap_or_default(),
231 None => get_default_contracts_file()
232 };
233 anyhow::anyhow!(
234 "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::<{}>()`.",
235 deployed_contract.key_name(), path, deployed_contract.name()
236 )
237 })?
238 .clone();
239 self.host_env.register_contract(
240 deployed_contract.address(),
241 deployed_contract.key_name(),
242 caller
243 );
244 }
245 Ok(())
246 }
247
248 fn dispatch(
253 &self,
254 cmd: &str,
255 args: &ArgMatches,
256 container: &mut DeployedContractsContainer
257 ) -> Result<()> {
258 match cmd {
259 DEPLOY_SUBCOMMAND => self
260 .deploy_cmd
261 .as_ref()
262 .ok_or_else(|| {
263 anyhow::anyhow!("Deploy command not found. Did you forget to add it?")
264 })?
265 .run(&self.host_env, args, &self.custom_types, container),
266 CONTRACTS_SUBCOMMAND => self.run_command(&self.contracts_cmd, args, container),
267 PRINT_EVENTS_SUBCOMMAND => self.run_command(&self.print_events_cmd, args, container),
268 SCENARIOS_SUBCOMMAND => self.run_command(&self.scenarios_cmd, args, container),
269 WHOAMI_SUBCOMMAND => self.run_command(&self.whoami_cmd, args, container),
270 STATUS_SUBCOMMAND => self.run_command(&self.status_cmd, args, container),
271 INSPECT_SUBCOMMAND => self.run_command(&self.inspect_cmd, args, container),
272 CONFIG_SUBCOMMAND => self.run_command(&self.config_cmd, args, container),
273 TRANSFER_SUBCOMMAND => self.run_command(&self.transfer_cmd, args, container),
274 COMPLETIONS_SUBCOMMAND => self
275 .completions_cmd
276 .generate(args, self.main_cmd.to_command(&[])),
277 REPL_SUBCOMMAND => repl::run(self, container),
278 _ => Err(anyhow::anyhow!("Unknown command: {cmd}"))
279 }
280 }
281
282 fn run_command<T: OdraCommand>(
283 &self,
284 cmd: &T,
285 args: &ArgMatches,
286 container: &DeployedContractsContainer
287 ) -> Result<()> {
288 cmd.run(&self.host_env, args, &self.custom_types, container)
289 }
290}