odra_cli/
lib.rs

1//! A rust library for building command line interfaces for Odra smart contracts.
2//!
3//! The Odra CLI is a command line interface built on top of the [clap] crate
4//! that allows users to interact with smart contracts.
5
6#![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    //! Traits and structs for defining custom scenarios.
40    //!
41    //! A scenario is a user-defined set of actions that can be run in the Odra CLI.
42    //! If you want to run a custom scenario that calls multiple entry points,
43    //! you need to implement the [Scenario] and [ScenarioMetadata] traits.
44    pub use crate::cmd::scenario::{
45        Scenario, ScenarioArgs as Args, ScenarioError as Error, ScenarioMetadata
46    };
47}
48
49pub mod deploy {
50    //! Traits and structs for defining deploy scripts.
51    //!
52    //! In a deploy script, you can define the contracts that you want to deploy to the blockchain
53    //! and write metadata to the container.
54    pub use crate::cmd::deploy::{DeployError as Error, DeployScript};
55}
56
57/// Command line interface for Odra smart contracts.
58pub 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    /// Creates a new empty instance of the Odra CLI.
75    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    /// Sets the description of the CLI
99    pub fn about(mut self, about: &str) -> Self {
100        self.main_cmd = self.main_cmd.about(about.to_string());
101        self
102    }
103
104    /// Adds a contract to the CLI.
105    ///
106    /// Generates a subcommand for the contract with all of its entry points except the `init` entry point.
107    /// To call the constructor of the contract, implement and register the [DeployScript].
108    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        // build entry points commands
122        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        // store a command
144        self.commands
145            .push(OdraCliCommand::new_contract::<T>(contract_name));
146        self
147    }
148
149    /// Adds a deploy script to the CLI.
150    ///
151    /// There is only one deploy script allowed in the CLI.
152    pub fn deploy(mut self, script: impl DeployScript + 'static) -> Self {
153        // register a subcommand for the deploy script
154        self.main_cmd = self
155            .main_cmd
156            .subcommand(command!(DEPLOY_SUBCOMMAND).about("Runs the deploy script"));
157        // store a command
158        self.commands.push(OdraCliCommand::new_deploy(script));
159        self
160    }
161
162    /// Adds a scenario to the CLI.
163    ///
164    /// Scenarios are user-defined commands that can be run from the CLI. If there
165    /// is a complex set of commands that need to be run in a specific order, a
166    /// scenario can be used to group them together.
167    pub fn scenario<S: ScenarioMetadata + Scenario>(mut self, scenario: S) -> Self {
168        // register a subcommand for the scenario
169        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        // store a command
182        self.commands.push(OdraCliCommand::new_scenario(scenario));
183        self
184    }
185
186    /// Builds the CLI.
187    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    /// Runs the CLI and parses the input.
194    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}