1use fastcrypto::encoding::Base58;
5use fastcrypto::encoding::Base64;
6use move_core_types::annotated_value::MoveDatatypeLayout;
7use move_core_types::identifier::Identifier;
8use move_core_types::language_storage::StructTag;
9use mysten_metrics::monitored_scope;
10use schemars::JsonSchema;
11use serde::{Deserialize, Serialize};
12use serde_json::{Value, json};
13use serde_with::{DisplayFromStr, serde_as};
14use std::fmt;
15use std::fmt::Display;
16use sui_types::base_types::{ObjectID, SuiAddress, TransactionDigest};
17use sui_types::error::SuiResult;
18use sui_types::event::{Event, EventEnvelope, EventID};
19use sui_types::sui_serde::BigInt;
20
21use json_to_table::json_to_table;
22use tabled::settings::Style as TableStyle;
23
24use crate::{Page, type_and_fields_from_move_event_data};
25use sui_types::sui_serde::SuiStructTag;
26
27use std::str::FromStr;
28
29pub type EventPage = Page<SuiEvent, EventID>;
30
31#[serde_as]
32#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize, JsonSchema)]
33#[serde(rename = "Event", rename_all = "camelCase")]
34pub struct SuiEvent {
35 pub id: EventID,
41 pub package_id: ObjectID,
43 #[schemars(with = "String")]
44 #[serde_as(as = "DisplayFromStr")]
45 pub transaction_module: Identifier,
47 pub sender: SuiAddress,
49 #[schemars(with = "String")]
50 #[serde_as(as = "SuiStructTag")]
51 pub type_: StructTag,
53 pub parsed_json: Value,
55 #[serde(flatten)]
57 pub bcs: BcsEvent,
58 #[serde(skip_serializing_if = "Option::is_none")]
60 #[schemars(with = "Option<BigInt<u64>>")]
61 #[serde_as(as = "Option<BigInt<u64>>")]
62 pub timestamp_ms: Option<u64>,
63}
64
65#[serde_as]
66#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize, JsonSchema)]
67#[serde(rename_all = "camelCase", tag = "bcsEncoding")]
68#[serde(from = "MaybeTaggedBcsEvent")]
69pub enum BcsEvent {
70 Base64 {
71 #[serde_as(as = "Base64")]
72 #[schemars(with = "Base64")]
73 bcs: Vec<u8>,
74 },
75 Base58 {
76 #[serde_as(as = "Base58")]
77 #[schemars(with = "Base58")]
78 bcs: Vec<u8>,
79 },
80}
81
82impl BcsEvent {
83 pub fn new(bytes: Vec<u8>) -> Self {
84 Self::Base64 { bcs: bytes }
85 }
86
87 pub fn bytes(&self) -> &[u8] {
88 match self {
89 BcsEvent::Base64 { bcs } => bcs.as_ref(),
90 BcsEvent::Base58 { bcs } => bcs.as_ref(),
91 }
92 }
93
94 pub fn into_bytes(self) -> Vec<u8> {
95 match self {
96 BcsEvent::Base64 { bcs } => bcs,
97 BcsEvent::Base58 { bcs } => bcs,
98 }
99 }
100}
101
102#[allow(unused)]
103#[serde_as]
104#[derive(Serialize, Deserialize)]
105#[serde(rename_all = "camelCase", untagged)]
106enum MaybeTaggedBcsEvent {
107 Tagged(TaggedBcsEvent),
108 Base58 {
109 #[serde_as(as = "Base58")]
110 bcs: Vec<u8>,
111 },
112}
113
114#[serde_as]
115#[derive(Serialize, Deserialize)]
116#[serde(rename_all = "camelCase", tag = "bcsEncoding")]
117enum TaggedBcsEvent {
118 Base64 {
119 #[serde_as(as = "Base64")]
120 bcs: Vec<u8>,
121 },
122 Base58 {
123 #[serde_as(as = "Base58")]
124 bcs: Vec<u8>,
125 },
126}
127
128impl From<MaybeTaggedBcsEvent> for BcsEvent {
129 fn from(event: MaybeTaggedBcsEvent) -> BcsEvent {
130 let bcs = match event {
131 MaybeTaggedBcsEvent::Tagged(TaggedBcsEvent::Base58 { bcs })
132 | MaybeTaggedBcsEvent::Base58 { bcs } => bcs,
133 MaybeTaggedBcsEvent::Tagged(TaggedBcsEvent::Base64 { bcs }) => bcs,
134 };
135
136 Self::Base64 { bcs }
138 }
139}
140
141impl From<EventEnvelope> for SuiEvent {
142 fn from(ev: EventEnvelope) -> Self {
143 Self {
144 id: EventID {
145 tx_digest: ev.tx_digest,
146 event_seq: ev.event_num,
147 },
148 package_id: ev.event.package_id,
149 transaction_module: ev.event.transaction_module,
150 sender: ev.event.sender,
151 type_: ev.event.type_,
152 parsed_json: ev.parsed_json,
153 bcs: BcsEvent::Base64 {
154 bcs: ev.event.contents,
155 },
156 timestamp_ms: Some(ev.timestamp),
157 }
158 }
159}
160
161impl From<SuiEvent> for Event {
162 fn from(val: SuiEvent) -> Self {
163 Event {
164 package_id: val.package_id,
165 transaction_module: val.transaction_module,
166 sender: val.sender,
167 type_: val.type_,
168 contents: val.bcs.into_bytes(),
169 }
170 }
171}
172
173impl SuiEvent {
174 pub fn try_from(
175 event: Event,
176 tx_digest: TransactionDigest,
177 event_seq: u64,
178 timestamp_ms: Option<u64>,
179 layout: MoveDatatypeLayout,
180 ) -> SuiResult<Self> {
181 let Event {
182 package_id,
183 transaction_module,
184 sender,
185 type_: _,
186 contents,
187 } = event;
188
189 let bcs = BcsEvent::Base64 {
190 bcs: contents.to_vec(),
191 };
192
193 let move_value = Event::move_event_to_move_value(&contents, layout)?;
194 let (type_, fields) = type_and_fields_from_move_event_data(move_value)?;
195
196 Ok(SuiEvent {
197 id: EventID {
198 tx_digest,
199 event_seq,
200 },
201 package_id,
202 transaction_module,
203 sender,
204 type_,
205 parsed_json: fields,
206 bcs,
207 timestamp_ms,
208 })
209 }
210}
211
212impl Display for SuiEvent {
213 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214 let parsed_json = &mut self.parsed_json.clone();
215 bytes_array_to_base64(parsed_json);
216 let mut table = json_to_table(parsed_json);
217 let style = TableStyle::modern();
218 table.collapse().with(style);
219 write!(
220 f,
221 " ┌──\n │ EventID: {}:{}\n │ PackageID: {}\n │ Transaction Module: {}\n │ Sender: {}\n │ EventType: {}\n",
222 self.id.tx_digest,
223 self.id.event_seq,
224 self.package_id,
225 self.transaction_module,
226 self.sender,
227 self.type_
228 )?;
229 if let Some(ts) = self.timestamp_ms {
230 writeln!(f, " │ Timestamp: {}\n └──", ts)?;
231 }
232 writeln!(f, " │ ParsedJSON:")?;
233 let table_string = table.to_string();
234 let table_rows = table_string.split_inclusive('\n');
235 for r in table_rows {
236 write!(f, " │ {r}")?;
237 }
238
239 write!(f, "\n └──")
240 }
241}
242
243impl SuiEvent {
244 pub fn random_for_testing() -> Self {
245 Self {
246 id: EventID {
247 tx_digest: TransactionDigest::random(),
248 event_seq: 0,
249 },
250 package_id: ObjectID::random(),
251 transaction_module: Identifier::from_str("random_for_testing").unwrap(),
252 sender: SuiAddress::random_for_testing_only(),
253 type_: StructTag::from_str("0x6666::random_for_testing::RandomForTesting").unwrap(),
254 parsed_json: json!({}),
255 bcs: BcsEvent::new(vec![]),
256 timestamp_ms: None,
257 }
258 }
259}
260
261fn bytes_array_to_base64(v: &mut Value) {
263 match v {
264 Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => (),
265 Value::Array(vals) => {
266 if let Some(vals) = vals.iter().map(try_into_byte).collect::<Option<Vec<_>>>() {
267 *v = json!(Base64::from_bytes(&vals).encoded())
268 } else {
269 for val in vals {
270 bytes_array_to_base64(val)
271 }
272 }
273 }
274 Value::Object(map) => {
275 for val in map.values_mut() {
276 bytes_array_to_base64(val)
277 }
278 }
279 }
280}
281
282fn try_into_byte(v: &Value) -> Option<u8> {
284 let num = v.as_u64()?;
285 (num <= 255).then_some(num as u8)
286}
287
288#[serde_as]
289#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
290pub enum EventFilter {
291 All([Box<EventFilter>; 0]),
293
294 Any(Vec<EventFilter>),
296
297 Sender(SuiAddress),
299 Transaction(
301 TransactionDigest,
303 ),
304 MoveModule {
309 package: ObjectID,
311 #[schemars(with = "String")]
313 #[serde_as(as = "DisplayFromStr")]
314 module: Identifier,
315 },
316 MoveEventType(
320 #[schemars(with = "String")]
321 #[serde_as(as = "SuiStructTag")]
322 StructTag,
323 ),
324 MoveEventModule {
329 package: ObjectID,
331 #[schemars(with = "String")]
333 #[serde_as(as = "DisplayFromStr")]
334 module: Identifier,
335 },
336 #[serde(rename_all = "camelCase")]
338 TimeRange {
339 #[schemars(with = "BigInt<u64>")]
341 #[serde_as(as = "BigInt<u64>")]
342 start_time: u64,
343 #[schemars(with = "BigInt<u64>")]
345 #[serde_as(as = "BigInt<u64>")]
346 end_time: u64,
347 },
348}
349
350impl Filter<SuiEvent> for EventFilter {
351 fn matches(&self, item: &SuiEvent) -> bool {
352 let _scope = monitored_scope("EventFilter::matches");
353 match self {
354 EventFilter::All([]) => true,
355 EventFilter::Any(filters) => filters.iter().any(|f| f.matches(item)),
356 EventFilter::MoveEventType(event_type) => &item.type_ == event_type,
357 EventFilter::Sender(sender) => &item.sender == sender,
358 EventFilter::MoveModule { package, module } => {
359 &item.transaction_module == module && &item.package_id == package
360 }
361 EventFilter::Transaction(digest) => digest == &item.id.tx_digest,
362
363 EventFilter::TimeRange {
364 start_time,
365 end_time,
366 } => {
367 if let Some(timestamp) = &item.timestamp_ms {
368 start_time <= timestamp && end_time > timestamp
369 } else {
370 false
371 }
372 }
373 EventFilter::MoveEventModule { package, module } => {
374 &item.type_.module == module && &ObjectID::from(item.type_.address) == package
375 }
376 }
377 }
378}
379
380pub trait Filter<T> {
381 fn matches(&self, item: &T) -> bool;
382}
383
384#[cfg(test)]
385mod test {
386 use super::*;
387
388 #[test]
389 fn bcs_event_test() {
390 let bytes = vec![0, 1, 2, 3, 4];
391 let untagged_base58 = r#"{"bcs":"12VfUX"}"#;
392 let tagged_base58 = r#"{"bcsEncoding":"base58","bcs":"12VfUX"}"#;
393 let tagged_base64 = r#"{"bcsEncoding":"base64","bcs":"AAECAwQ="}"#;
394
395 assert_eq!(
396 bytes,
397 serde_json::from_str::<BcsEvent>(untagged_base58)
398 .unwrap()
399 .into_bytes()
400 );
401 assert_eq!(
402 bytes,
403 serde_json::from_str::<BcsEvent>(tagged_base58)
404 .unwrap()
405 .into_bytes()
406 );
407 assert_eq!(
408 bytes,
409 serde_json::from_str::<BcsEvent>(tagged_base64)
410 .unwrap()
411 .into_bytes()
412 );
413
414 let event = serde_json::from_str::<BcsEvent>(tagged_base64).unwrap();
416 let json = serde_json::to_string(&event).unwrap();
417 let from_json = serde_json::from_str::<BcsEvent>(&json).unwrap();
418 assert_eq!(event, from_json);
419
420 let event = serde_json::from_str::<BcsEvent>(tagged_base58).unwrap();
422 let json = serde_json::to_string(&event).unwrap();
423 let from_json = serde_json::from_str::<BcsEvent>(&json).unwrap();
424 assert_eq!(event, from_json);
425 }
426}