use clap::{arg, command, Parser};
use std::io;
use soroban_env_host::xdr::{self, Limits, ReadXdr};
use super::{global, NetworkRunnable};
use crate::config::{self, locator, network};
use crate::rpc;
#[derive(Parser, Debug, Clone)]
#[group(skip)]
pub struct Cmd {
#[allow(clippy::doc_markdown)]
#[arg(long, conflicts_with = "cursor", required_unless_present = "cursor")]
start_ledger: Option<u32>,
#[arg(
long,
conflicts_with = "start_ledger",
required_unless_present = "start_ledger"
)]
cursor: Option<String>,
#[arg(long, value_enum, default_value = "pretty")]
output: OutputFormat,
#[arg(short, long, default_value = "10")]
count: usize,
#[arg(
long = "id",
num_args = 1..=6,
help_heading = "FILTERS"
)]
contract_ids: Vec<String>,
#[arg(
long = "topic",
num_args = 1..=5,
help_heading = "FILTERS"
)]
topic_filters: Vec<String>,
#[arg(
long = "type",
value_enum,
default_value = "all",
help_heading = "FILTERS"
)]
event_type: rpc::EventType,
#[command(flatten)]
locator: locator::Args,
#[command(flatten)]
network: network::Args,
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("cursor is not valid")]
InvalidCursor,
#[error("filepath does not exist: {path}")]
InvalidFile { path: String },
#[error("filepath ({path}) cannot be read: {error}")]
CannotReadFile { path: String, error: String },
#[error("cannot parse topic filter {topic} into 1-4 segments")]
InvalidTopicFilter { topic: String },
#[error("invalid segment ({segment}) in topic filter ({topic}): {error}")]
InvalidSegment {
topic: String,
segment: String,
error: xdr::Error,
},
#[error("cannot parse contract ID {contract_id}: {error}")]
InvalidContractId {
contract_id: String,
error: stellar_strkey::DecodeError,
},
#[error("invalid JSON string: {error} ({debug})")]
InvalidJson {
debug: String,
error: serde_json::Error,
},
#[error("invalid timestamp in event: {ts}")]
InvalidTimestamp { ts: String },
#[error("missing start_ledger and cursor")]
MissingStartLedgerAndCursor,
#[error("missing target")]
MissingTarget,
#[error(transparent)]
Rpc(#[from] rpc::Error),
#[error(transparent)]
Generic(#[from] Box<dyn std::error::Error>),
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
Xdr(#[from] xdr::Error),
#[error(transparent)]
Serde(#[from] serde_json::Error),
#[error(transparent)]
Network(#[from] network::Error),
#[error(transparent)]
Locator(#[from] locator::Error),
#[error(transparent)]
Config(#[from] config::Error),
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, clap::ValueEnum)]
pub enum OutputFormat {
Pretty,
Plain,
Json,
}
impl Cmd {
pub async fn run(&mut self) -> Result<(), Error> {
for topic in &self.topic_filters {
for (i, segment) in topic.split(',').enumerate() {
if i > 4 {
return Err(Error::InvalidTopicFilter {
topic: topic.to_string(),
});
}
if segment != "*" {
if let Err(e) = xdr::ScVal::from_xdr_base64(segment, Limits::none()) {
return Err(Error::InvalidSegment {
topic: topic.to_string(),
segment: segment.to_string(),
error: e,
});
}
}
}
}
let response = self.run_against_rpc_server(None, None).await?;
for event in &response.events {
match self.output {
OutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&event).map_err(|e| {
Error::InvalidJson {
debug: format!("{event:#?}"),
error: e,
}
})?,
);
}
OutputFormat::Plain => println!("{event}"),
OutputFormat::Pretty => event.pretty_print()?,
}
}
println!("Latest Ledger: {}", response.latest_ledger);
Ok(())
}
fn start(&self) -> Result<rpc::EventStart, Error> {
let start = match (self.start_ledger, self.cursor.clone()) {
(Some(start), _) => rpc::EventStart::Ledger(start),
(_, Some(c)) => rpc::EventStart::Cursor(c),
_ => return Err(Error::MissingStartLedgerAndCursor),
};
Ok(start)
}
}
#[async_trait::async_trait]
impl NetworkRunnable for Cmd {
type Error = Error;
type Result = rpc::GetEventsResponse;
async fn run_against_rpc_server(
&self,
_args: Option<&global::Args>,
config: Option<&config::Args>,
) -> Result<rpc::GetEventsResponse, Error> {
let start = self.start()?;
let network = if let Some(config) = config {
Ok(config.get_network()?)
} else {
self.network.get(&self.locator)
}?;
let client = rpc::Client::new(&network.rpc_url)?;
client
.verify_network_passphrase(Some(&network.network_passphrase))
.await?;
let contract_ids: Vec<String> = self
.contract_ids
.iter()
.map(|id| {
Ok(self
.locator
.resolve_contract_id(id, &network.network_passphrase)?
.to_string())
})
.collect::<Result<Vec<_>, Error>>()?;
Ok(client
.get_events(
start,
Some(self.event_type),
&contract_ids,
&self.topic_filters,
Some(self.count),
)
.await
.map_err(Error::Rpc)?)
}
}