Skip to main content

odra_cli/
cli.rs

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
35/// Command line interface for Odra smart contracts.
36pub 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    /// Creates a new empty instance of the Odra CLI.
62    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    /// Sets the description of the CLI
84    pub fn about(mut self, about: &'static str) -> Self {
85        self.main_cmd = self.main_cmd.about(about);
86        self
87    }
88
89    /// Sets the path to a file that stores the deployed contract addresses.
90    ///
91    /// The path is relative to the resources directory in the project root.
92    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    /// Adds a contract to the CLI.
98    ///
99    /// Generates a subcommand for the contract with all of its entry points except the `init` entry point.
100    /// To call the constructor of the contract, implement and register the [DeployScript].
101    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    /// Adds a named contract to the CLI, in case of multiple instances of the same contract.
117    ///
118    /// Generates a subcommand for the contract with all of its entry points except the `init` entry point.
119    /// To call the constructor of the contract, implement and register the [DeployScript].
120    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    /// Adds a deploy script to the CLI.
139    ///
140    /// There is only one deploy script allowed in the CLI.
141    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    /// Adds a scenario to the CLI.
149    ///
150    /// Scenarios are user-defined commands that can be run from the CLI. If there
151    /// is a complex set of commands that need to be run in a specific order, a
152    /// scenario can be used to group them together.
153    pub fn scenario<S: ScenarioMetadata + Scenario>(mut self, scenario: S) -> Self {
154        self.scenarios_cmd.add_scenario(scenario);
155        self
156    }
157
158    /// Builds the CLI.
159    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    /// Runs the CLI once, parsing the input from the process arguments and exiting on error.
177    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        // Init contracts container with the provided path or default to the resources directory.
185        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        // Register the contracts from the container in the host environment.
193        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    /// Builds the deployed-contracts container from the given path (or the default location).
205    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    /// Registers every deployed contract from the container in the host environment.
215    ///
216    /// Only contracts that have a matching caller (added via `.contract::<T>()` in the builder) can
217    /// be registered. Returns an error naming the missing caller instead of exiting, so the REPL can
218    /// report it without killing the session.
219    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    /// Dispatches a parsed subcommand to its handler.
249    ///
250    /// `deploy` mutates the container; the other subcommands are read-only. An unknown verb returns
251    /// an error (recoverable in the REPL) rather than panicking.
252    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}