use crate::{
cmd::{
args::{read_arg, Arg, ArgsError},
OdraCommand, PRINT_EVENTS_SUBCOMMAND
},
container::{self, ContractProvider},
custom_types::CustomTypeSet,
types, DeployedContractsContainer
};
use anyhow::Result;
use clap::{ArgMatches, Command};
use odra::{contract_def::HasIdent, host::HostEnv, OdraContract};
#[derive(Debug, thiserror::Error)]
pub enum EventError {
#[error(transparent)]
ArgsError(#[from] ArgsError),
#[error(transparent)]
TypesError(#[from] types::Error),
#[error("Contract not found")]
ContractNotFound,
#[error("Event at {index} not found for contract '{contract_name}'")]
EventNotFound { index: u32, contract_name: String },
#[error(transparent)]
ContractError(#[from] container::ContractError)
}
#[derive(Default)]
pub(crate) struct PrintEventsCmd {
subcommands: Vec<PrintContractEventsCmd>
}
impl PrintEventsCmd {
pub fn add_contract<T: OdraContract>(&mut self) {
self.subcommands
.push(PrintContractEventsCmd::new::<T>(None));
}
pub fn add_contract_named<T: OdraContract>(&mut self, package_name: String) {
self.subcommands
.push(PrintContractEventsCmd::new::<T>(Some(package_name)));
}
}
impl OdraCommand for PrintEventsCmd {
fn run(
&self,
env: &HostEnv,
args: &ArgMatches,
types: &CustomTypeSet,
container: &DeployedContractsContainer
) -> Result<()> {
let (subcmd, args) = args.subcommand().ok_or(EventError::ContractNotFound)?;
if let Some(cmd) = self.subcommands.iter().find(|c| c.contract_name == subcmd) {
cmd.run(env, args, types, container)
} else {
Err(EventError::ContractNotFound.into())
}
}
}
impl From<&PrintEventsCmd> for Command {
fn from(value: &PrintEventsCmd) -> Self {
Command::new(PRINT_EVENTS_SUBCOMMAND)
.about("Prints the most recent events emitted by a contract")
.arg_required_else_help(true)
.subcommand_required(true)
.subcommands(&value.subcommands)
}
}
struct PrintContractEventsCmd {
contract_name: String
}
impl PrintContractEventsCmd {
fn new<T: OdraContract>(package_name: Option<String>) -> Self {
let contract_name = package_name.unwrap_or_else(T::HostRef::ident);
Self { contract_name }
}
}
impl OdraCommand for PrintContractEventsCmd {
fn run(
&self,
env: &HostEnv,
args: &ArgMatches,
types: &CustomTypeSet,
container: &DeployedContractsContainer
) -> Result<()> {
env.set_captures_events(true);
let contract_address = container
.address_by_name(&self.contract_name)
.ok_or(EventError::ContractNotFound)?;
let events_count = env.events_count(&contract_address);
let max_events = read_arg(args, Arg::EventsNumber)
.unwrap_or(events_count)
.min(events_count);
prettycli::info(&format!(
"Printing {} most recent events for contract '{}'",
max_events, self.contract_name
));
for i in 0..max_events {
let idx = events_count - i - 1;
let ev = env.get_event_bytes(&contract_address, idx).map_err(|_| {
EventError::EventNotFound {
index: i,
contract_name: self.contract_name.clone()
}
})?;
prettycli::info(&format!(
"Event {}: {}",
i + 1,
types::decode_event(&ev, types)?
));
}
Ok(())
}
}
impl From<&PrintContractEventsCmd> for Command {
fn from(value: &PrintContractEventsCmd) -> Self {
Command::new(&value.contract_name)
.about(format!(
"Print events of the {} contract",
&value.contract_name
))
.arg(Arg::EventsNumber)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{cmd::args::ARG_NUMBER, test_utils::TestContract};
#[test]
fn test_print_events_cmd() {
let mut cmd = PrintEventsCmd::default();
let command: Command = (&cmd).into();
assert_eq!(command.get_name(), PRINT_EVENTS_SUBCOMMAND);
assert_eq!(command.get_subcommands().count(), 0);
cmd.add_contract::<TestContract>();
let command: Command = (&cmd).into();
assert_eq!(command.get_subcommands().count(), 1);
}
#[test]
fn test_match_print_events_cmd() {
let mut cmd = PrintEventsCmd::default();
cmd.add_contract::<TestContract>();
let command: Command = (&cmd).into();
let matches = command
.try_get_matches_from(vec![PRINT_EVENTS_SUBCOMMAND, &TestContract::ident()])
.unwrap();
let (subcommand, _) = matches.subcommand().expect("Subcommand should be present");
assert_eq!(subcommand, TestContract::ident());
}
#[test]
fn parsing_print_events_cmd_invalid_contract() {
let mut cmd = PrintEventsCmd::default();
cmd.add_contract::<TestContract>();
let command: Command = (&cmd).into();
let matches = command.try_get_matches_from(vec![PRINT_EVENTS_SUBCOMMAND, "TestContract2"]);
assert_eq!(
matches.unwrap_err().kind(),
clap::error::ErrorKind::InvalidSubcommand
);
}
#[test]
fn parsing_number_of_events() {
let cmd = PrintContractEventsCmd::new::<TestContract>(None);
let command: Command = (&cmd).into();
let matches = command.get_matches_from(vec!["TestContract", "--number", "5"]);
assert_eq!(*matches.get_one::<u32>(ARG_NUMBER).unwrap(), 5);
}
#[test]
fn parsing_default_number_of_events() {
let cmd = PrintContractEventsCmd::new::<TestContract>(None);
let command: Command = (&cmd).into();
let matches = command.try_get_matches_from(vec!["TestContract"]);
assert!(matches.is_ok());
let matches = matches.unwrap();
assert!(matches.contains_id(ARG_NUMBER));
assert_eq!(*matches.get_one::<u32>(ARG_NUMBER).unwrap(), 10);
}
#[test]
fn parsing_default_number_of_events_with_invalid_value() {
let cmd = PrintContractEventsCmd::new::<TestContract>(None);
let command: Command = (&cmd).into();
let matches = command.try_get_matches_from(vec!["TestContract", "--number", "invalid"]);
assert!(matches.is_err());
assert_eq!(
matches.unwrap_err().kind(),
clap::error::ErrorKind::ValueValidation
);
}
}