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