iota_json_rpc_types/
iota_event.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use 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/// Unique ID of an IOTA Event, the ID is a combination of transaction digest
35/// and event seq number.
36#[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    /// Sequential event ID, ie (transaction seq number, event seq number).
71    /// 1) Serves as a unique event ID for each fullnode
72    /// 2) Also serves to sequence events for the purposes of pagination and
73    ///    querying. A higher id is an event seen later by that fullnode.
74    /// This ID is the "cursor" for event querying.
75    #[schemars(with = "IotaEventID")]
76    pub id: EventID,
77    /// Move package where this event was emitted.
78    #[schemars(with = "ObjectIDSchema")]
79    pub package_id: ObjectID,
80    #[schemars(with = "IdentifierSchema")]
81    /// Move module where this event was emitted.
82    pub transaction_module: Identifier,
83    /// Sender's IOTA address.
84    #[schemars(with = "IotaAddressSchema")]
85    pub sender: IotaAddress,
86    /// Move event type.
87    #[schemars(with = "StructTagSchema")]
88    #[serde_as(as = "StructTagSchema")]
89    pub type_: StructTag,
90    /// Parsed json value of the event
91    pub parsed_json: Value,
92    /// Base64 encoded bcs bytes of the move event
93    #[serde(flatten)]
94    pub bcs: BcsEvent,
95    /// UTC timestamp in milliseconds since epoch (1/1/1970)
96    #[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        // Bytes are already decoded, force into Base64 variant to avoid serializing to
174        // base58
175        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
302/// Convert a json array of bytes to Base64
303fn 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
323/// Try to convert a json Value object into an u8.
324fn 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    /// Query by sender address.
333    Sender(#[schemars(with = "IotaAddressSchema")] IotaAddress),
334    /// Return events emitted by the given transaction.
335    Transaction(
336        /// digest of the transaction, as base-64 encoded string
337        #[schemars(with = "Base58Schema")]
338        TransactionDigest,
339    ),
340    /// Return events emitted in a specified Package.
341    Package(#[schemars(with = "ObjectIDSchema")] ObjectID),
342    /// Return events emitted in a specified Move module.
343    /// If the event is defined in Module A but emitted in a tx with Module B,
344    /// query `MoveModule` by module B returns the event.
345    /// Query `MoveEventModule` by module A returns the event too.
346    MoveModule {
347        /// the Move package ID
348        #[schemars(with = "ObjectIDSchema")]
349        package: ObjectID,
350        /// the module name
351        #[schemars(with = "IdentifierSchema")]
352        module: Identifier,
353    },
354    /// Return events with the given Move event struct name (struct tag).
355    /// For example, if the event is defined in `0xabcd::MyModule`, and named
356    /// `Foo`, then the struct tag is `0xabcd::MyModule::Foo`.
357    MoveEventType(
358        #[schemars(with = "StructTagSchema")]
359        #[serde_as(as = "StructTagSchema")]
360        StructTag,
361    ),
362    /// Return events with the given Move module name where the event struct is
363    /// defined. If the event is defined in Module A but emitted in a tx
364    /// with Module B, query `MoveEventModule` by module A returns the
365    /// event. Query `MoveModule` by module B returns the event too.
366    MoveEventModule {
367        /// the Move package ID
368        #[schemars(with = "ObjectIDSchema")]
369        package: ObjectID,
370        /// the module name
371        #[schemars(with = "IdentifierSchema")]
372        module: Identifier,
373    },
374    MoveEventField {
375        path: String,
376        value: Value,
377    },
378    /// Return events emitted in [start_time, end_time] interval
379    #[serde(rename_all = "camelCase")]
380    TimeRange {
381        /// left endpoint of time interval, milliseconds since epoch, inclusive
382        #[serde_as(as = "DisplayFromStr")]
383        #[schemars(with = "String")]
384        start_time: u64,
385        /// right endpoint of time interval, milliseconds since epoch, exclusive
386        #[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        // Roundtrip base64
485        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        // Roundtrip base58
491        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}