odra_cli/
cli.rs

1use std::collections::HashMap;
2
3use anyhow::Result;
4use clap::ArgMatches;
5use odra::{
6    contract_def::HasIdent,
7    entry_point_callback::EntryPointsCaller,
8    host::{EntryPointsCallerProvider, HostEnv},
9    schema::{SchemaCustomTypes, SchemaEntrypoints, SchemaEvents},
10    OdraContract
11};
12
13use crate::{
14    cmd::{
15        ContractsCmd, DeployCmd, DeployScript, MainCmd, MutableCommand, OdraCommand,
16        PrintEventsCmd, Scenario, ScenarioMetadata, ScenariosCmd, WhoamiCmd, CONTRACTS_SUBCOMMAND,
17        DEPLOY_SUBCOMMAND, PRINT_EVENTS_SUBCOMMAND, SCENARIOS_SUBCOMMAND, WHOAMI_SUBCOMMAND
18    },
19    container::{FileContractStorage, DEPLOYED_CONTRACTS_FILE},
20    custom_types::CustomTypes,
21    ContractProvider, DeployedContractsContainer
22};
23
24/// Command line interface for Odra smart contracts.
25pub struct OdraCli {
26    main_cmd: MainCmd,
27    deploy_cmd: Option<DeployCmd>,
28    contracts_cmd: ContractsCmd,
29    print_events_cmd: PrintEventsCmd,
30    scenarios_cmd: ScenariosCmd,
31    whoami_cmd: WhoamiCmd,
32    custom_types: CustomTypes,
33    host_env: HostEnv,
34    callers: HashMap<String, EntryPointsCaller>
35}
36
37impl Default for OdraCli {
38    fn default() -> Self {
39        Self::new()
40    }
41}
42
43impl OdraCli {
44    /// Creates a new empty instance of the Odra CLI.
45    pub fn new() -> Self {
46        Self {
47            main_cmd: MainCmd::default(),
48            deploy_cmd: None,
49            contracts_cmd: ContractsCmd::default(),
50            print_events_cmd: PrintEventsCmd::default(),
51            scenarios_cmd: ScenariosCmd::default(),
52            whoami_cmd: WhoamiCmd::new(),
53            host_env: odra_casper_livenet_env::env(),
54            custom_types: CustomTypes::default(),
55            callers: HashMap::default()
56        }
57    }
58
59    #[cfg(test)]
60    pub fn test(host_env: HostEnv) -> Self {
61        Self {
62            main_cmd: MainCmd::default(),
63            deploy_cmd: None,
64            contracts_cmd: ContractsCmd::default(),
65            print_events_cmd: PrintEventsCmd::default(),
66            scenarios_cmd: ScenariosCmd::default(),
67            whoami_cmd: WhoamiCmd::new(),
68            host_env,
69            custom_types: CustomTypes::default(),
70            callers: HashMap::default()
71        }
72    }
73
74    /// Sets the description of the CLI
75    pub fn about(mut self, about: &'static str) -> Self {
76        self.main_cmd = self.main_cmd.about(about);
77        self
78    }
79
80    /// Adds a contract to the CLI.
81    ///
82    /// Generates a subcommand for the contract with all of its entry points except the `init` entry point.
83    /// To call the constructor of the contract, implement and register the [DeployScript].
84    pub fn contract<T: SchemaEntrypoints + SchemaCustomTypes + SchemaEvents + OdraContract>(
85        mut self
86    ) -> Self {
87        self.callers.insert(
88            T::HostRef::ident(),
89            T::HostRef::entry_points_caller(&self.host_env)
90        );
91        self.custom_types.register::<T>();
92        self.contracts_cmd.add_contract::<T>();
93        self.print_events_cmd.add_contract::<T>();
94        self
95    }
96
97    /// Adds a deploy script to the CLI.
98    ///
99    /// There is only one deploy script allowed in the CLI.
100    pub fn deploy(mut self, script: impl DeployScript + 'static) -> Self {
101        let cmd = DeployCmd::new(script);
102        self.main_cmd = self.main_cmd.subcommand(&cmd);
103        self.deploy_cmd = Some(cmd);
104        self
105    }
106
107    /// Adds a scenario to the CLI.
108    ///
109    /// Scenarios are user-defined commands that can be run from the CLI. If there
110    /// is a complex set of commands that need to be run in a specific order, a
111    /// scenario can be used to group them together.
112    pub fn scenario<S: ScenarioMetadata + Scenario>(mut self, scenario: S) -> Self {
113        self.scenarios_cmd.add_scenario(scenario);
114        self
115    }
116
117    /// Builds the CLI.
118    pub fn build(mut self) -> Self {
119        self.main_cmd = self.main_cmd.subcommand(&self.contracts_cmd);
120        self.main_cmd = self.main_cmd.subcommand(&self.scenarios_cmd);
121        self.main_cmd = self.main_cmd.subcommand(&self.print_events_cmd);
122        self.main_cmd = self.main_cmd.subcommand(&self.whoami_cmd);
123        self
124    }
125
126    /// Runs the CLI and parses the input.
127    pub fn run(self) {
128        let (cmd, args, contracts_path) = self.main_cmd.get_matches();
129
130        let storage = FileContractStorage::new(contracts_path.clone()).unwrap_or_else(|e| {
131            prettycli::error(&format!("Failed to create contract storage: {e}"));
132            std::process::exit(1);
133        });
134        // Init contracts container with the provided path or default to the resources directory.
135        let mut container = DeployedContractsContainer::instance(storage);
136
137        // Register the contracts from the container in the host environment.
138        for (name, address) in container.all_contracts() {
139            let caller = self.callers.get(&name).unwrap_or_else(|| {
140                let path = match &contracts_path {
141                    Some(path) => path.to_str().map(|s| s.to_string()).unwrap_or_default(),
142                    None => DEPLOYED_CONTRACTS_FILE.to_string()
143                };
144                prettycli::error(&format!(
145                    "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::<{}>()`.",
146                    &name, path, &name
147                ));
148                std::process::exit(1);
149            }).clone();
150            self.host_env.register_contract(address, name, caller);
151        }
152
153        let result = match cmd.as_str() {
154            DEPLOY_SUBCOMMAND => self
155                .deploy_cmd
156                .as_ref()
157                .unwrap_or_else(|| {
158                    prettycli::error("Deploy command not found. Did you forget to add it?");
159                    std::process::exit(1);
160                })
161                .run(&self.host_env, &args, &self.custom_types, &mut container),
162            CONTRACTS_SUBCOMMAND => self.run_command(&self.contracts_cmd, args, &container),
163            PRINT_EVENTS_SUBCOMMAND => self.run_command(&self.print_events_cmd, args, &container),
164            SCENARIOS_SUBCOMMAND => self.run_command(&self.scenarios_cmd, args, &container),
165            WHOAMI_SUBCOMMAND => self.run_command(&self.whoami_cmd, args, &container),
166            _ => unreachable!()
167        };
168
169        match result {
170            Ok(_) => prettycli::info("Command executed successfully"),
171            Err(err) => prettycli::error(&format!("{:?}", err))
172        }
173    }
174
175    fn run_command<T: OdraCommand>(
176        &self,
177        cmd: &T,
178        args: ArgMatches,
179        container: &DeployedContractsContainer
180    ) -> Result<()> {
181        cmd.run(&self.host_env, &args, &self.custom_types, container)
182    }
183}