1use serde::{Deserialize, Serialize};
2
3use crate::client::SorobanRpcClient;
4use crate::error::RpcError;
5
6#[derive(Debug, Serialize, Deserialize)]
7pub struct ContractEvent {
8 pub contract_id: String,
9 pub ledger: Option<u32>,
10 pub topics: Vec<String>,
11 pub value: Option<String>,
12}
13
14pub type TopicFilter = Vec<String>;
18
19pub const TOPIC_WILDCARD_ONE: &str = "*";
21pub const TOPIC_WILDCARD_REST: &str = "**";
23
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
28#[serde(rename_all = "camelCase")]
29#[non_exhaustive]
30pub struct EventFilter {
31 #[serde(rename = "type")]
32 pub filter_type: String,
33 pub contract_ids: Vec<String>,
34 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub topics: Option<Vec<TopicFilter>>,
38}
39
40impl EventFilter {
41 pub fn contract<I, S>(contract_ids: I) -> Self
44 where
45 I: IntoIterator<Item = S>,
46 S: Into<String>,
47 {
48 Self {
49 filter_type: "contract".to_string(),
50 contract_ids: contract_ids.into_iter().map(Into::into).collect(),
51 topics: None,
52 }
53 }
54
55 pub fn with_topics(mut self, topics: Vec<TopicFilter>) -> Self {
57 self.topics = Some(topics);
58 self
59 }
60}
61
62pub fn first_topic_filter(first_topic_b64: impl Into<String>) -> TopicFilter {
65 vec![first_topic_b64.into(), TOPIC_WILDCARD_REST.to_string()]
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
69#[serde(rename_all = "camelCase")]
70pub struct GetEventsRequest {
71 pub start_ledger: u32,
72 #[serde(skip_serializing_if = "Option::is_none")]
73 pub end_ledger: Option<u32>,
74 pub filters: Vec<EventFilter>,
75}
76
77#[derive(Deserialize)]
78struct GetEventsResponse {
79 events: Vec<RpcEvent>,
80}
81
82#[derive(Deserialize)]
83#[allow(non_snake_case, dead_code)]
84struct RpcEvent {
85 ledger: u32,
90 contractId: String,
91 topic: Vec<String>,
92 value: String,
95}
96
97pub fn resolve_ledger_range(
105 start_ledger: Option<u32>,
106 end_ledger: Option<u32>,
107 latest_ledger: u32,
108) -> Result<(u32, Option<u32>), RpcError> {
109 let start = match start_ledger {
110 Some(s) => s,
111 None => {
112 if let Some(end) = end_ledger {
113 end.saturating_sub(1000).max(1)
114 } else {
115 latest_ledger.saturating_sub(1000).max(1)
116 }
117 }
118 };
119
120 if let Some(end) = end_ledger {
121 if start > end {
122 return Err(RpcError::Rpc(format!(
123 "Invalid ledger range: start ledger ({start}) cannot be greater than end ledger ({end})"
124 )));
125 }
126 }
127
128 Ok((start, end_ledger))
129}
130
131pub async fn get_contract_events(
132 client: &SorobanRpcClient,
133 contract_id: &str,
134 start_ledger: Option<u32>,
135 end_ledger: Option<u32>,
136) -> Result<Vec<ContractEvent>, RpcError> {
137 get_contract_events_filtered(client, contract_id, start_ledger, end_ledger, None).await
138}
139
140pub async fn get_contract_events_filtered(
143 client: &SorobanRpcClient,
144 contract_id: &str,
145 start_ledger: Option<u32>,
146 end_ledger: Option<u32>,
147 topics: Option<Vec<TopicFilter>>,
148) -> Result<Vec<ContractEvent>, RpcError> {
149 if let (Some(start), Some(end)) = (start_ledger, end_ledger) {
151 if start > end {
152 return Err(RpcError::Rpc(format!(
153 "Invalid ledger range: start ledger ({start}) cannot be greater than end ledger ({end})"
154 )));
155 }
156 }
157
158 let (start_ledger, end_ledger) = if start_ledger.is_none() && end_ledger.is_none() {
161 let latest_ledger_info =
162 client
163 .get_ledger()
164 .await
165 .unwrap_or_else(|_| crate::client::LedgerInfo {
166 id: "".to_string(),
167 protocol_version: 0,
168 sequence: 1000,
169 });
170 resolve_ledger_range(start_ledger, end_ledger, latest_ledger_info.sequence)?
171 } else {
172 resolve_ledger_range(start_ledger, end_ledger, 0)?
173 };
174
175 let request_end_ledger = match end_ledger {
178 Some(end) => Some(end.checked_add(1).ok_or_else(|| {
179 RpcError::Rpc(format!(
180 "End ledger ({end}) exceeds maximum supported ledger sequence"
181 ))
182 })?),
183 None => None,
184 };
185
186 let request_body = GetEventsRequest {
187 start_ledger,
188 end_ledger: request_end_ledger,
189 filters: vec![EventFilter {
190 topics,
191 ..EventFilter::contract([contract_id])
192 }],
193 };
194
195 let result: GetEventsResponse = client.request("getEvents", request_body).await?;
196
197 let mut contract_events = Vec::new();
198
199 for ev in result.events {
200 contract_events.push(ContractEvent {
201 contract_id: ev.contractId,
202 ledger: Some(ev.ledger),
203 topics: ev.topic,
204 value: Some(ev.value),
205 });
206 }
207
208 Ok(contract_events)
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
216 fn test_resolve_ledger_range_defaults() {
217 let (start, end) = resolve_ledger_range(None, None, 2500).unwrap();
219 assert_eq!(start, 1500);
220 assert_eq!(end, None);
221
222 let (start, end) = resolve_ledger_range(None, None, 500).unwrap();
224 assert_eq!(start, 1);
225 assert_eq!(end, None);
226 }
227
228 #[test]
229 fn test_resolve_ledger_range_explicit_range() {
230 let (start, end) = resolve_ledger_range(Some(1000), Some(2000), 5000).unwrap();
231 assert_eq!(start, 1000);
232 assert_eq!(end, Some(2000));
233
234 let (start, end) = resolve_ledger_range(Some(1000), Some(1000), 5000).unwrap();
236 assert_eq!(start, 1000);
237 assert_eq!(end, Some(1000));
238 }
239
240 #[test]
241 fn test_resolve_ledger_range_start_only() {
242 let (start, end) = resolve_ledger_range(Some(1000), None, 5000).unwrap();
244 assert_eq!(start, 1000);
245 assert_eq!(end, None);
246 }
247
248 #[test]
249 fn test_resolve_ledger_range_end_only() {
250 let (start, end) = resolve_ledger_range(None, Some(2500), 5000).unwrap();
252 assert_eq!(start, 1500);
253 assert_eq!(end, Some(2500));
254
255 let (start, end) = resolve_ledger_range(None, Some(500), 5000).unwrap();
256 assert_eq!(start, 1);
257 assert_eq!(end, Some(500));
258 }
259
260 #[test]
261 fn test_resolve_ledger_range_rejects_inverted() {
262 let err = resolve_ledger_range(Some(5000), Some(1000), 10000).unwrap_err();
263 match err {
264 RpcError::Rpc(msg) => {
265 assert!(
266 msg.contains("start ledger (5000) cannot be greater than end ledger (1000)")
267 );
268 }
269 other => panic!("Unexpected error variant: {other:?}"),
270 }
271 }
272
273 #[test]
274 fn test_resolve_ledger_range_rejects_end_zero() {
275 let err = resolve_ledger_range(None, Some(0), 10000).unwrap_err();
276 match err {
277 RpcError::Rpc(msg) => {
278 assert!(msg.contains("start ledger (1) cannot be greater than end ledger (0)"));
279 }
280 other => panic!("Unexpected error variant: {other:?}"),
281 }
282 }
283
284 #[test]
285 fn test_get_events_request_serialization_with_end_ledger() {
286 let req = GetEventsRequest {
287 start_ledger: 1000,
288 end_ledger: Some(2000),
289 filters: vec![EventFilter {
290 filter_type: "contract".to_string(),
291 contract_ids: vec![
292 "CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5CWVMOMG2P3T2YYW".to_string()
293 ],
294 topics: None,
295 }],
296 };
297
298 let json = serde_json::to_string(&req).unwrap();
299 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
300
301 assert_eq!(value["startLedger"], 1000);
302 assert_eq!(value["endLedger"], 2000);
303 assert_eq!(value["filters"][0]["type"], "contract");
304 assert_eq!(
305 value["filters"][0]["contractIds"][0],
306 "CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5CWVMOMG2P3T2YYW"
307 );
308 }
309
310 #[test]
311 fn test_get_events_request_serialization_omits_none_end_ledger() {
312 let req = GetEventsRequest {
313 start_ledger: 1000,
314 end_ledger: None,
315 filters: vec![EventFilter {
316 filter_type: "contract".to_string(),
317 contract_ids: vec![
318 "CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5CWVMOMG2P3T2YYW".to_string()
319 ],
320 topics: None,
321 }],
322 };
323
324 let json = serde_json::to_string(&req).unwrap();
325 assert!(json.contains("\"startLedger\":1000"));
326 assert!(!json.contains("endLedger"));
327
328 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
329 assert_eq!(value["startLedger"], 1000);
330 assert!(value.get("endLedger").is_none());
331 }
332
333 #[test]
334 fn test_event_filter_omits_topics_when_absent() {
335 let filter = EventFilter {
336 filter_type: "contract".to_string(),
337 contract_ids: vec![
338 "CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5CWVMOMG2P3T2YYW".to_string()
339 ],
340 topics: None,
341 };
342 assert_eq!(
344 serde_json::to_string(&filter).unwrap(),
345 r#"{"type":"contract","contractIds":["CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5CWVMOMG2P3T2YYW"]}"#
346 );
347 }
348
349 #[test]
350 fn test_event_filter_serializes_topics_when_present() {
351 let filter = EventFilter {
352 filter_type: "contract".to_string(),
353 contract_ids: vec![
354 "CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5CWVMOMG2P3T2YYW".to_string()
355 ],
356 topics: Some(vec![first_topic_filter("AAAADwAAAAhUcmFuc2Zlcg==")]),
357 };
358 let value = serde_json::to_value(&filter).unwrap();
359 assert_eq!(
360 value["topics"],
361 serde_json::json!([["AAAADwAAAAhUcmFuc2Zlcg==", "**"]])
362 );
363 assert_eq!(value["type"], "contract");
364
365 let back: EventFilter = serde_json::from_value(value).unwrap();
367 assert_eq!(back, filter);
368 let legacy: EventFilter =
369 serde_json::from_str(r#"{"type":"contract","contractIds":[]}"#).unwrap();
370 assert_eq!(legacy.topics, None);
371 }
372
373 #[test]
374 fn test_event_filter_constructors() {
375 let id = "CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5CWVMOMG2P3T2YYW";
376 let plain = EventFilter::contract([id]);
377 assert_eq!(plain.filter_type, "contract");
378 assert_eq!(plain.contract_ids, vec![id.to_string()]);
379 assert_eq!(plain.topics, None);
380
381 let filtered = EventFilter::contract([id]).with_topics(vec![first_topic_filter("AAAA")]);
382 assert_eq!(
383 filtered.topics,
384 Some(vec![vec!["AAAA".to_string(), "**".to_string()]])
385 );
386 }
387
388 #[test]
389 fn test_first_topic_filter_matches_any_trailing_topics() {
390 assert_eq!(
391 first_topic_filter("AAAA"),
392 vec!["AAAA".to_string(), TOPIC_WILDCARD_REST.to_string()]
393 );
394 }
395
396 #[tokio::test]
397 async fn test_get_contract_events_rejects_inverted_range_before_network() {
398 let client = SorobanRpcClient::new("http://127.0.0.1:9999");
399 let result = get_contract_events(
400 &client,
401 "CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5CWVMOMG2P3T2YYW",
402 Some(5000),
403 Some(1000),
404 )
405 .await;
406
407 let err = result.unwrap_err();
408 match err {
409 RpcError::Rpc(msg) => {
410 assert!(
411 msg.contains("start ledger (5000) cannot be greater than end ledger (1000)")
412 );
413 }
414 other => panic!("Unexpected error variant: {other:?}"),
415 }
416 }
417
418 #[tokio::test]
419 async fn test_get_contract_events_rejects_u32_max_end_ledger_overflow() {
420 let client = SorobanRpcClient::new("http://127.0.0.1:9999");
421 let result = get_contract_events(
422 &client,
423 "CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5CWVMOMG2P3T2YYW",
424 Some(1000),
425 Some(u32::MAX),
426 )
427 .await;
428
429 let err = result.unwrap_err();
430 match err {
431 RpcError::Rpc(msg) => {
432 assert!(msg.contains("exceeds maximum supported ledger sequence"));
433 }
434 other => panic!("Unexpected error variant: {other:?}"),
435 }
436 }
437}