1use clap::Parser;
2use std::io;
3
4use crate::xdr::{self, Limits, ReadXdr};
5use crate::{
6 config::{self, locator, network},
7 rpc,
8};
9
10#[derive(Parser, Debug, Clone)]
11#[group(skip)]
12pub struct Cmd {
13 #[allow(clippy::doc_markdown)]
14 #[arg(long, conflicts_with = "cursor", required_unless_present = "cursor")]
17 start_ledger: Option<u32>,
18 #[arg(
20 long,
21 conflicts_with = "start_ledger",
22 required_unless_present = "start_ledger"
23 )]
24 cursor: Option<String>,
25 #[arg(long, value_enum, default_value = "pretty")]
27 output: OutputFormat,
28 #[arg(short, long, default_value = "10")]
30 count: usize,
31 #[arg(
39 long = "id",
40 num_args = 1..=6,
41 help_heading = "FILTERS"
42 )]
43 contract_ids: Vec<config::UnresolvedContract>,
44 #[arg(
61 long = "topic",
62 num_args = 1.., help_heading = "FILTERS"
64 )]
65 topic_filters: Vec<String>,
66 #[arg(
68 long = "type",
69 value_enum,
70 default_value = "all",
71 help_heading = "FILTERS"
72 )]
73 event_type: rpc::EventType,
74 #[command(flatten)]
75 locator: locator::Args,
76 #[command(flatten)]
77 network: network::Args,
78}
79
80#[derive(thiserror::Error, Debug)]
81pub enum Error {
82 #[error("cursor is not valid")]
83 InvalidCursor,
84 #[error("filepath does not exist: {path}")]
85 InvalidFile { path: String },
86 #[error("filepath ({path}) cannot be read: {error}")]
87 CannotReadFile { path: String, error: String },
88 #[error("max of 5 topic filters allowed per request, received {filter_count}")]
89 MaxTopicFilters { filter_count: usize },
90 #[error("cannot parse topic filter {topic} into 1-4 segments")]
91 InvalidTopicFilter { topic: String },
92 #[error("invalid segment ({segment}) in topic filter ({topic}): {error}")]
93 InvalidSegment {
94 topic: String,
95 segment: String,
96 error: xdr::Error,
97 },
98 #[error("cannot parse contract ID {contract_id}: {error}")]
99 InvalidContractId {
100 contract_id: String,
101 error: stellar_strkey::DecodeError,
102 },
103 #[error("invalid JSON string: {error} ({debug})")]
104 InvalidJson {
105 debug: String,
106 error: serde_json::Error,
107 },
108 #[error("invalid timestamp in event: {ts}")]
109 InvalidTimestamp { ts: String },
110 #[error("missing start_ledger and cursor")]
111 MissingStartLedgerAndCursor,
112 #[error("missing target")]
113 MissingTarget,
114 #[error(transparent)]
115 Rpc(#[from] rpc::Error),
116 #[error(transparent)]
117 Generic(#[from] Box<dyn std::error::Error>),
118 #[error(transparent)]
119 Io(#[from] io::Error),
120 #[error(transparent)]
121 Xdr(#[from] xdr::Error),
122 #[error(transparent)]
123 Serde(#[from] serde_json::Error),
124 #[error(transparent)]
125 Network(#[from] network::Error),
126 #[error(transparent)]
127 Locator(#[from] locator::Error),
128 #[error(transparent)]
129 Config(#[from] config::Error),
130}
131
132#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, clap::ValueEnum)]
133pub enum OutputFormat {
134 Pretty,
136 Plain,
138 Json,
140}
141
142impl Cmd {
143 pub async fn run(&mut self) -> Result<(), Error> {
144 let response = self
145 .execute(&config::Args {
146 locator: self.locator.clone(),
147 network: self.network.clone(),
148 source_account: config::UnresolvedMuxedAccount::default(),
149 sign_with: config::sign_with::Args::default(),
150 fee: None,
151 inclusion_fee: None,
152 })
153 .await?;
154
155 if response.events.is_empty() {
156 eprintln!("No events");
157 }
158
159 for event in &response.events {
160 match self.output {
161 OutputFormat::Json => {
165 println!(
166 "{}",
167 serde_json::to_string_pretty(&event).map_err(|e| {
168 Error::InvalidJson {
169 debug: format!("{event:#?}"),
170 error: e,
171 }
172 })?,
173 );
174 }
175 OutputFormat::Plain => println!("{event}"),
176 OutputFormat::Pretty => event.pretty_print()?,
177 }
178 }
179 Ok(())
180 }
181
182 pub async fn execute(&self, config: &config::Args) -> Result<rpc::GetEventsResponse, Error> {
183 let start = self.start()?;
184 let network = config.get_network()?;
185 let client = network.rpc_client()?;
186 client
187 .verify_network_passphrase(Some(&network.network_passphrase))
188 .await?;
189
190 let contract_ids: Vec<String> = self
191 .contract_ids
192 .iter()
193 .map(|id| {
194 Ok(id
195 .resolve_contract_id(&self.locator, &network.network_passphrase)?
196 .to_string())
197 })
198 .collect::<Result<Vec<_>, Error>>()?;
199
200 let parsed_topics = self.parse_topics()?;
201
202 client
203 .get_events(
204 start,
205 Some(self.event_type),
206 &contract_ids,
207 &parsed_topics,
208 Some(self.count),
209 )
210 .await
211 .map_err(Error::Rpc)
212 }
213
214 fn parse_topics(&self) -> Result<Vec<rpc::TopicFilter>, Error> {
215 if self.topic_filters.len() > 5 {
216 return Err(Error::MaxTopicFilters {
217 filter_count: self.topic_filters.len(),
218 });
219 }
220 let mut topic_filters: Vec<rpc::TopicFilter> = Vec::new();
221 for topic in &self.topic_filters {
222 let mut topic_filter: rpc::TopicFilter = Vec::new(); for (i, segment) in topic.split(',').enumerate() {
224 if i > 4 {
225 return Err(Error::InvalidTopicFilter {
226 topic: topic.clone(),
227 });
228 }
229
230 if segment == "*" || segment == "**" {
231 topic_filter.push(segment.to_owned());
232 } else {
233 match xdr::ScVal::from_xdr_base64(segment, Limits::none()) {
234 Ok(_s) => {
235 topic_filter.push(segment.to_owned());
236 }
237 Err(e) => {
238 return Err(Error::InvalidSegment {
239 topic: topic.clone(),
240 segment: segment.to_string(),
241 error: e,
242 });
243 }
244 }
245 }
246 }
247 topic_filters.push(topic_filter);
248 }
249
250 Ok(topic_filters)
251 }
252
253 fn start(&self) -> Result<rpc::EventStart, Error> {
254 let start = match (self.start_ledger, self.cursor.clone()) {
255 (Some(start), _) => rpc::EventStart::Ledger(start),
256 (_, Some(c)) => rpc::EventStart::Cursor(c),
257 _ => return Err(Error::MissingStartLedgerAndCursor),
259 };
260 Ok(start)
261 }
262}