1use std::{fmt, fmt::Display, str::FromStr};
6
7use fastcrypto::encoding::{Base58, Base64};
8use iota_metrics::monitored_scope;
9use iota_types::{
10 base_types::{Identifier, IotaAddress, ObjectID, StructTag, TransactionDigest},
11 error::IotaResult,
12 event::{Event, EventEnvelope, EventID},
13 object::bounded_visitor::BoundedVisitor,
14};
15use json_to_table::json_to_table;
16use move_core_types::annotated_value::MoveDatatypeLayout;
17use schemars::JsonSchema;
18use serde::{Deserialize, Serialize};
19use serde_json::{Value, json};
20use serde_with::{DisplayFromStr, serde_as};
21use tabled::settings::Style as TableStyle;
22
23use crate::{
24 Page,
25 iota_primitives::{
26 Base58 as Base58Schema, Base64 as Base64Schema, Identifier as IdentifierSchema,
27 IotaAddress as IotaAddressSchema, ObjectID as ObjectIDSchema, StructTag as StructTagSchema,
28 },
29 type_and_fields_from_move_event_data,
30};
31
32pub type EventPage = Page<IotaEvent, EventID>;
33
34#[serde_as]
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, JsonSchema)]
38#[serde(rename_all = "camelCase")]
39#[schemars(rename = "EventID")]
40pub struct IotaEventID {
41 #[schemars(with = "Base58Schema")]
42 pub tx_digest: TransactionDigest,
43 #[schemars(with = "String")]
44 #[serde_as(as = "DisplayFromStr")]
45 pub event_seq: u64,
46}
47
48impl From<EventID> for IotaEventID {
49 fn from(id: EventID) -> Self {
50 Self {
51 tx_digest: id.tx_digest,
52 event_seq: id.event_seq,
53 }
54 }
55}
56
57impl From<IotaEventID> for EventID {
58 fn from(id: IotaEventID) -> Self {
59 Self {
60 tx_digest: id.tx_digest,
61 event_seq: id.event_seq,
62 }
63 }
64}
65
66#[serde_as]
67#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize, JsonSchema)]
68#[serde(rename = "Event", rename_all = "camelCase")]
69pub struct IotaEvent {
70 #[schemars(with = "IotaEventID")]
76 pub id: EventID,
77 #[schemars(with = "ObjectIDSchema")]
79 pub package_id: ObjectID,
80 #[schemars(with = "IdentifierSchema")]
81 pub transaction_module: Identifier,
83 #[schemars(with = "IotaAddressSchema")]
85 pub sender: IotaAddress,
86 #[schemars(with = "StructTagSchema")]
88 #[serde_as(as = "StructTagSchema")]
89 pub type_: StructTag,
90 pub parsed_json: Value,
92 #[serde(flatten)]
94 pub bcs: BcsEvent,
95 #[serde(skip_serializing_if = "Option::is_none")]
97 #[schemars(with = "Option<String>")]
98 #[serde_as(as = "Option<DisplayFromStr>")]
99 pub timestamp_ms: Option<u64>,
100}
101
102#[serde_as]
103#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize, JsonSchema)]
104#[serde(rename_all = "camelCase", tag = "bcsEncoding")]
105#[serde(from = "MaybeTaggedBcsEvent")]
106pub enum BcsEvent {
107 Base64 {
108 #[serde_as(as = "Base64")]
109 #[schemars(with = "Base64Schema")]
110 bcs: Vec<u8>,
111 },
112 Base58 {
113 #[serde_as(as = "Base58")]
114 #[schemars(with = "Base58Schema")]
115 bcs: Vec<u8>,
116 },
117}
118
119impl BcsEvent {
120 pub fn new(bytes: Vec<u8>) -> Self {
121 Self::Base64 { bcs: bytes }
122 }
123
124 pub fn bytes(&self) -> &[u8] {
125 match self {
126 BcsEvent::Base64 { bcs } => bcs.as_ref(),
127 BcsEvent::Base58 { bcs } => bcs.as_ref(),
128 }
129 }
130
131 pub fn into_bytes(self) -> Vec<u8> {
132 match self {
133 BcsEvent::Base64 { bcs } => bcs,
134 BcsEvent::Base58 { bcs } => bcs,
135 }
136 }
137}
138
139#[allow(unused)]
140#[serde_as]
141#[derive(Serialize, Deserialize)]
142#[serde(rename_all = "camelCase", untagged)]
143enum MaybeTaggedBcsEvent {
144 Tagged(TaggedBcsEvent),
145 Base58 {
146 #[serde_as(as = "Base58")]
147 bcs: Vec<u8>,
148 },
149}
150
151#[serde_as]
152#[derive(Serialize, Deserialize)]
153#[serde(rename_all = "camelCase", tag = "bcsEncoding")]
154enum TaggedBcsEvent {
155 Base64 {
156 #[serde_as(as = "Base64")]
157 bcs: Vec<u8>,
158 },
159 Base58 {
160 #[serde_as(as = "Base58")]
161 bcs: Vec<u8>,
162 },
163}
164
165impl From<MaybeTaggedBcsEvent> for BcsEvent {
166 fn from(event: MaybeTaggedBcsEvent) -> BcsEvent {
167 let bcs = match event {
168 MaybeTaggedBcsEvent::Tagged(TaggedBcsEvent::Base58 { bcs })
169 | MaybeTaggedBcsEvent::Base58 { bcs } => bcs,
170 MaybeTaggedBcsEvent::Tagged(TaggedBcsEvent::Base64 { bcs }) => bcs,
171 };
172
173 Self::Base64 { bcs }
176 }
177}
178
179impl From<EventEnvelope> for IotaEvent {
180 fn from(ev: EventEnvelope) -> Self {
181 Self {
182 id: EventID {
183 tx_digest: ev.tx_digest,
184 event_seq: ev.event_num,
185 },
186 package_id: ev.event.package_id,
187 transaction_module: ev.event.module,
188 sender: ev.event.sender,
189 type_: ev.event.type_,
190 parsed_json: ev.parsed_json,
191 bcs: BcsEvent::Base64 {
192 bcs: ev.event.contents,
193 },
194 timestamp_ms: Some(ev.timestamp),
195 }
196 }
197}
198
199impl From<IotaEvent> for Event {
200 fn from(val: IotaEvent) -> Self {
201 Event {
202 package_id: val.package_id,
203 module: val.transaction_module,
204 sender: val.sender,
205 type_: val.type_,
206 contents: val.bcs.into_bytes(),
207 }
208 }
209}
210
211impl IotaEvent {
212 pub fn try_from(
213 event: Event,
214 tx_digest: TransactionDigest,
215 event_seq: u64,
216 timestamp_ms: Option<u64>,
217 layout: MoveDatatypeLayout,
218 ) -> IotaResult<Self> {
219 let Event {
220 package_id,
221 module,
222 sender,
223 type_: _,
224 contents,
225 } = event;
226
227 let bcs = BcsEvent::Base64 {
228 bcs: contents.to_vec(),
229 };
230
231 let move_value = BoundedVisitor::deserialize_value(&contents, &layout.into_layout())
232 .map_err(|e| iota_types::error::IotaError::ObjectDeserialization {
233 error: e.to_string(),
234 })?;
235 let (type_, fields) = type_and_fields_from_move_event_data(move_value)?;
236
237 Ok(IotaEvent {
238 id: EventID {
239 tx_digest,
240 event_seq,
241 },
242 package_id,
243 transaction_module: module,
244 sender,
245 type_,
246 parsed_json: fields,
247 bcs,
248 timestamp_ms,
249 })
250 }
251}
252
253impl Display for IotaEvent {
254 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255 let parsed_json = &mut self.parsed_json.clone();
256 bytes_array_to_base64(parsed_json);
257 let mut table = json_to_table(parsed_json);
258 let style = TableStyle::modern();
259 table.collapse().with(style);
260 write!(
261 f,
262 " ┌──\n │ EventID: {}:{}\n │ PackageID: {}\n │ Transaction Module: {}\n │ Sender: {}\n │ EventType: {}\n",
263 self.id.tx_digest,
264 self.id.event_seq,
265 self.package_id,
266 self.transaction_module,
267 self.sender,
268 self.type_
269 )?;
270 if let Some(ts) = self.timestamp_ms {
271 writeln!(f, " │ Timestamp: {ts}\n └──")?;
272 }
273 writeln!(f, " │ ParsedJSON:")?;
274 let table_string = table.to_string();
275 let table_rows = table_string.split_inclusive('\n');
276 for r in table_rows {
277 write!(f, " │ {r}")?;
278 }
279
280 write!(f, "\n └──")
281 }
282}
283
284impl IotaEvent {
285 pub fn random_for_testing() -> Self {
286 Self {
287 id: EventID {
288 tx_digest: TransactionDigest::random(),
289 event_seq: 0,
290 },
291 package_id: ObjectID::random(),
292 transaction_module: Identifier::from_str("random_for_testing").unwrap(),
293 sender: IotaAddress::random(),
294 type_: StructTag::from_str("0x6666::random_for_testing::RandomForTesting").unwrap(),
295 parsed_json: json!({}),
296 bcs: BcsEvent::new(vec![]),
297 timestamp_ms: None,
298 }
299 }
300}
301
302fn bytes_array_to_base64(v: &mut Value) {
304 match v {
305 Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => (),
306 Value::Array(vals) => {
307 if let Some(vals) = vals.iter().map(try_into_byte).collect::<Option<Vec<_>>>() {
308 *v = json!(Base64::from_bytes(&vals).encoded())
309 } else {
310 for val in vals {
311 bytes_array_to_base64(val)
312 }
313 }
314 }
315 Value::Object(map) => {
316 for val in map.values_mut() {
317 bytes_array_to_base64(val)
318 }
319 }
320 }
321}
322
323fn try_into_byte(v: &Value) -> Option<u8> {
325 let num = v.as_u64()?;
326 (num <= 255).then_some(num as u8)
327}
328
329#[serde_as]
330#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
331pub enum EventFilter {
332 Sender(#[schemars(with = "IotaAddressSchema")] IotaAddress),
334 Transaction(
336 #[schemars(with = "Base58Schema")]
338 TransactionDigest,
339 ),
340 Package(#[schemars(with = "ObjectIDSchema")] ObjectID),
342 MoveModule {
347 #[schemars(with = "ObjectIDSchema")]
349 package: ObjectID,
350 #[schemars(with = "IdentifierSchema")]
352 module: Identifier,
353 },
354 MoveEventType(
358 #[schemars(with = "StructTagSchema")]
359 #[serde_as(as = "StructTagSchema")]
360 StructTag,
361 ),
362 MoveEventModule {
367 #[schemars(with = "ObjectIDSchema")]
369 package: ObjectID,
370 #[schemars(with = "IdentifierSchema")]
372 module: Identifier,
373 },
374 MoveEventField {
375 path: String,
376 value: Value,
377 },
378 #[serde(rename_all = "camelCase")]
380 TimeRange {
381 #[serde_as(as = "DisplayFromStr")]
383 #[schemars(with = "String")]
384 start_time: u64,
385 #[serde_as(as = "DisplayFromStr")]
387 #[schemars(with = "String")]
388 end_time: u64,
389 },
390
391 All(Vec<EventFilter>),
392 Any(Vec<EventFilter>),
393 And(Box<EventFilter>, Box<EventFilter>),
394 Or(Box<EventFilter>, Box<EventFilter>),
395}
396
397impl EventFilter {
398 fn try_matches(&self, item: &IotaEvent) -> IotaResult<bool> {
399 Ok(match self {
400 EventFilter::MoveEventType(event_type) => &item.type_ == event_type,
401 EventFilter::MoveEventField { path, value } => {
402 matches!(item.parsed_json.pointer(path), Some(v) if v == value)
403 }
404 EventFilter::Sender(sender) => &item.sender == sender,
405 EventFilter::Package(object_id) => &item.package_id == object_id,
406 EventFilter::MoveModule { package, module } => {
407 &item.transaction_module == module && &item.package_id == package
408 }
409 EventFilter::All(filters) => filters.iter().all(|f| f.matches(item)),
410 EventFilter::Any(filters) => filters.iter().any(|f| f.matches(item)),
411 EventFilter::And(f1, f2) => {
412 EventFilter::All(vec![*(*f1).clone(), *(*f2).clone()]).matches(item)
413 }
414 EventFilter::Or(f1, f2) => {
415 EventFilter::Any(vec![*(*f1).clone(), *(*f2).clone()]).matches(item)
416 }
417 EventFilter::Transaction(digest) => digest == &item.id.tx_digest,
418
419 EventFilter::TimeRange {
420 start_time,
421 end_time,
422 } => {
423 if let Some(timestamp) = &item.timestamp_ms {
424 start_time <= timestamp && end_time > timestamp
425 } else {
426 false
427 }
428 }
429 EventFilter::MoveEventModule { package, module } => {
430 item.type_.module() == module && &item.type_.address() == package.as_address()
431 }
432 })
433 }
434
435 pub fn and(self, other_filter: EventFilter) -> Self {
436 Self::All(vec![self, other_filter])
437 }
438 pub fn or(self, other_filter: EventFilter) -> Self {
439 Self::Any(vec![self, other_filter])
440 }
441}
442
443impl Filter<IotaEvent> for EventFilter {
444 fn matches(&self, item: &IotaEvent) -> bool {
445 let _scope = monitored_scope("EventFilter::matches");
446 self.try_matches(item).unwrap_or_default()
447 }
448}
449
450pub trait Filter<T> {
451 fn matches(&self, item: &T) -> bool;
452}
453
454#[cfg(test)]
455mod test {
456 use super::*;
457
458 #[test]
459 fn bcs_event_test() {
460 let bytes = vec![0, 1, 2, 3, 4];
461 let untagged_base58 = r#"{"bcs":"12VfUX"}"#;
462 let tagged_base58 = r#"{"bcsEncoding":"base58","bcs":"12VfUX"}"#;
463 let tagged_base64 = r#"{"bcsEncoding":"base64","bcs":"AAECAwQ="}"#;
464
465 assert_eq!(
466 bytes,
467 serde_json::from_str::<BcsEvent>(untagged_base58)
468 .unwrap()
469 .into_bytes()
470 );
471 assert_eq!(
472 bytes,
473 serde_json::from_str::<BcsEvent>(tagged_base58)
474 .unwrap()
475 .into_bytes()
476 );
477 assert_eq!(
478 bytes,
479 serde_json::from_str::<BcsEvent>(tagged_base64)
480 .unwrap()
481 .into_bytes()
482 );
483
484 let event = serde_json::from_str::<BcsEvent>(tagged_base64).unwrap();
486 let json = serde_json::to_string(&event).unwrap();
487 let from_json = serde_json::from_str::<BcsEvent>(&json).unwrap();
488 assert_eq!(event, from_json);
489
490 let event = serde_json::from_str::<BcsEvent>(tagged_base58).unwrap();
492 let json = serde_json::to_string(&event).unwrap();
493 let from_json = serde_json::from_str::<BcsEvent>(&json).unwrap();
494 assert_eq!(event, from_json);
495 }
496}