sui_bridge/
events.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! This file contains the definition of the SuiBridgeEvent enum, of
5//! which each variant is an emitted Event struct defind in the Move
6//! Bridge module. We rely on structures in this file to decode
7//! the bcs content of the emitted events.
8
9#![allow(non_upper_case_globals)]
10
11use crate::crypto::BridgeAuthorityPublicKey;
12use crate::error::BridgeError;
13use crate::error::BridgeResult;
14use crate::types::BridgeAction;
15use crate::types::SuiToEthTokenTransfer;
16use crate::types::SuiToEthTokenTransferV2;
17use alloy::primitives::Address as EthAddress;
18use fastcrypto::encoding::Encoding;
19use fastcrypto::encoding::Hex;
20use move_core_types::language_storage::StructTag;
21use once_cell::sync::OnceCell;
22use serde::{Deserialize, Serialize};
23use std::str::FromStr;
24use sui_json_rpc_types::SuiEvent;
25use sui_types::BRIDGE_PACKAGE_ID;
26use sui_types::TypeTag;
27use sui_types::base_types::SuiAddress;
28use sui_types::bridge::BridgeChainId;
29use sui_types::bridge::MoveTypeBridgeMessageKey;
30use sui_types::bridge::MoveTypeCommitteeMember;
31use sui_types::bridge::MoveTypeCommitteeMemberRegistration;
32use sui_types::collection_types::VecMap;
33use sui_types::crypto::ToFromBytes;
34use sui_types::event::Event;
35use sui_types::parse_sui_type_tag;
36
37// `TokendDepositedEvent` emitted in bridge.move
38#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
39pub struct MoveTokenDepositedEvent {
40    pub seq_num: u64,
41    pub source_chain: u8,
42    pub sender_address: Vec<u8>,
43    pub target_chain: u8,
44    pub target_address: Vec<u8>,
45    pub token_type: u8,
46    pub amount_sui_adjusted: u64,
47}
48
49// `TokendDepositedEventV2` emitted in bridge.move
50#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
51pub struct MoveTokenDepositedEventV2 {
52    pub seq_num: u64,
53    pub source_chain: u8,
54    pub sender_address: Vec<u8>,
55    pub target_chain: u8,
56    pub target_address: Vec<u8>,
57    pub token_type: u8,
58    pub amount_sui_adjusted: u64,
59    pub timestamp_ms: u64,
60}
61
62macro_rules! new_move_event {
63    ($struct_name:ident, $move_struct_name:ident) => {
64
65        // `$move_struct_name` emitted in bridge.move
66        #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
67        pub struct $move_struct_name {
68            pub message_key: MoveTypeBridgeMessageKey,
69        }
70
71        // Sanitized version of the given `move_struct_name`
72        #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Hash)]
73        pub struct $struct_name {
74            pub nonce: u64,
75            pub source_chain: BridgeChainId,
76        }
77
78        impl TryFrom<$move_struct_name> for $struct_name {
79            type Error = BridgeError;
80
81            fn try_from(event: $move_struct_name) -> BridgeResult<Self> {
82                let source_chain = BridgeChainId::try_from(event.message_key.source_chain).map_err(|_e| {
83                    BridgeError::Generic(format!(
84                        "Failed to convert {} to {}. Failed to convert source chain {} to BridgeChainId",
85                        stringify!($move_struct_name),
86                        stringify!($struct_name),
87                        event.message_key.source_chain,
88                    ))
89                })?;
90                Ok(Self {
91                    nonce: event.message_key.bridge_seq_num,
92                    source_chain,
93                })
94            }
95        }
96    };
97}
98
99new_move_event!(TokenTransferClaimed, MoveTokenTransferClaimed);
100new_move_event!(TokenTransferApproved, MoveTokenTransferApproved);
101new_move_event!(
102    TokenTransferAlreadyApproved,
103    MoveTokenTransferAlreadyApproved
104);
105new_move_event!(TokenTransferAlreadyClaimed, MoveTokenTransferAlreadyClaimed);
106new_move_event!(TokenTransferLimitExceed, MoveTokenTransferLimitExceed);
107
108// `EmergencyOpEvent` emitted in bridge.move
109#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
110pub struct EmergencyOpEvent {
111    pub frozen: bool,
112}
113
114// `CommitteeUpdateEvent` emitted in committee.move
115#[derive(Debug, Serialize, Deserialize, Clone)]
116pub struct MoveCommitteeUpdateEvent {
117    pub members: VecMap<Vec<u8>, MoveTypeCommitteeMember>,
118    pub stake_participation_percentage: u64,
119}
120
121// `CommitteeMemberUrlUpdateEvent` emitted in committee.move
122#[derive(Debug, Serialize, Deserialize, Clone)]
123pub struct MoveCommitteeMemberUrlUpdateEvent {
124    pub member: Vec<u8>,
125    pub new_url: Vec<u8>,
126}
127
128// `BlocklistValidatorEvent` emitted in committee.move
129#[derive(Debug, Serialize, Deserialize, Clone)]
130pub struct MoveBlocklistValidatorEvent {
131    pub blocklisted: bool,
132    pub public_keys: Vec<Vec<u8>>,
133}
134
135// `UpdateRouteLimitEvent` emitted in limiter.move
136#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
137pub struct UpdateRouteLimitEvent {
138    pub sending_chain: u8,
139    pub receiving_chain: u8,
140    pub new_limit: u64,
141}
142
143// `TokenRegistrationEvent` emitted in treasury.move
144#[derive(Debug, Serialize, Deserialize, Clone)]
145pub struct MoveTokenRegistrationEvent {
146    pub type_name: String,
147    pub decimal: u8,
148    pub native_token: bool,
149}
150
151// Sanitized version of MoveTokenRegistrationEvent
152#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
153pub struct TokenRegistrationEvent {
154    pub type_name: TypeTag,
155    pub decimal: u8,
156    pub native_token: bool,
157}
158
159impl TryFrom<MoveTokenRegistrationEvent> for TokenRegistrationEvent {
160    type Error = BridgeError;
161
162    fn try_from(event: MoveTokenRegistrationEvent) -> BridgeResult<Self> {
163        let type_name = parse_sui_type_tag(&format!("0x{}", event.type_name)).map_err(|e| {
164            BridgeError::InternalError(format!(
165                "Failed to parse TypeTag: {e}, type name: {}",
166                event.type_name
167            ))
168        })?;
169
170        Ok(Self {
171            type_name,
172            decimal: event.decimal,
173            native_token: event.native_token,
174        })
175    }
176}
177
178// `NewTokenEvent` emitted in treasury.move
179#[derive(Debug, Serialize, Deserialize, Clone)]
180pub struct MoveNewTokenEvent {
181    pub token_id: u8,
182    pub type_name: String,
183    pub native_token: bool,
184    pub decimal_multiplier: u64,
185    pub notional_value: u64,
186}
187
188// Sanitized version of MoveNewTokenEvent
189#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
190pub struct NewTokenEvent {
191    pub token_id: u8,
192    pub type_name: TypeTag,
193    pub native_token: bool,
194    pub decimal_multiplier: u64,
195    pub notional_value: u64,
196}
197
198impl TryFrom<MoveNewTokenEvent> for NewTokenEvent {
199    type Error = BridgeError;
200
201    fn try_from(event: MoveNewTokenEvent) -> BridgeResult<Self> {
202        let type_name = parse_sui_type_tag(&format!("0x{}", event.type_name)).map_err(|e| {
203            BridgeError::InternalError(format!(
204                "Failed to parse TypeTag: {e}, type name: {}",
205                event.type_name
206            ))
207        })?;
208
209        Ok(Self {
210            token_id: event.token_id,
211            type_name,
212            native_token: event.native_token,
213            decimal_multiplier: event.decimal_multiplier,
214            notional_value: event.notional_value,
215        })
216    }
217}
218
219// `UpdateTokenPriceEvent` emitted in treasury.move
220#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
221pub struct UpdateTokenPriceEvent {
222    pub token_id: u8,
223    pub new_price: u64,
224}
225
226// Sanitized version of MoveTokenDepositedEvent
227#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Hash)]
228pub struct EmittedSuiToEthTokenBridgeV1 {
229    pub nonce: u64,
230    pub sui_chain_id: BridgeChainId,
231    pub eth_chain_id: BridgeChainId,
232    pub sui_address: SuiAddress,
233    pub eth_address: EthAddress,
234    pub token_id: u8,
235    // The amount of tokens deposited with decimal points on Sui side
236    pub amount_sui_adjusted: u64,
237}
238
239// Sanitized version of MoveTokenDepositedEventV2
240#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Hash)]
241pub struct EmittedSuiToEthTokenBridgeV2 {
242    pub nonce: u64,
243    pub sui_chain_id: BridgeChainId,
244    pub eth_chain_id: BridgeChainId,
245    pub sui_address: SuiAddress,
246    pub eth_address: EthAddress,
247    pub token_id: u8,
248    // The amount of tokens deposited with decimal points on Sui side
249    pub amount_sui_adjusted: u64,
250    pub timestamp_ms: u64,
251}
252
253// Sanitized version of MoveCommitteeUpdateEvent
254#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
255pub struct CommitteeUpdate {
256    pub members: Vec<MoveTypeCommitteeMember>,
257    pub stake_participation_percentage: u64,
258}
259
260impl TryFrom<MoveCommitteeUpdateEvent> for CommitteeUpdate {
261    type Error = BridgeError;
262
263    fn try_from(event: MoveCommitteeUpdateEvent) -> BridgeResult<Self> {
264        let members = event
265            .members
266            .contents
267            .into_iter()
268            .map(|v| v.value)
269            .collect();
270        Ok(Self {
271            members,
272            stake_participation_percentage: event.stake_participation_percentage,
273        })
274    }
275}
276
277// Sanitized version of MoveBlocklistValidatorEvent
278#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
279pub struct BlocklistValidatorEvent {
280    pub blocklisted: bool,
281    pub public_keys: Vec<BridgeAuthorityPublicKey>,
282}
283
284impl TryFrom<MoveBlocklistValidatorEvent> for BlocklistValidatorEvent {
285    type Error = BridgeError;
286
287    fn try_from(event: MoveBlocklistValidatorEvent) -> BridgeResult<Self> {
288        let public_keys = event.public_keys.into_iter().map(|bytes|
289            BridgeAuthorityPublicKey::from_bytes(&bytes).map_err(|e|
290                BridgeError::Generic(format!("Failed to convert MoveBlocklistValidatorEvent to BlocklistValidatorEvent. Failed to convert public key to BridgeAuthorityPublicKey: {:?}", e))
291            )
292        ).collect::<BridgeResult<Vec<_>>>()?;
293        Ok(Self {
294            blocklisted: event.blocklisted,
295            public_keys,
296        })
297    }
298}
299
300// Sanitized version of MoveCommitteeMemberUrlUpdateEvent
301#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
302pub struct CommitteeMemberUrlUpdateEvent {
303    pub member: BridgeAuthorityPublicKey,
304    pub new_url: String,
305}
306
307impl TryFrom<MoveCommitteeMemberUrlUpdateEvent> for CommitteeMemberUrlUpdateEvent {
308    type Error = BridgeError;
309
310    fn try_from(event: MoveCommitteeMemberUrlUpdateEvent) -> BridgeResult<Self> {
311        let member = BridgeAuthorityPublicKey::from_bytes(&event.member).map_err(|e|
312            BridgeError::Generic(format!("Failed to convert MoveBlocklistValidatorEvent to BlocklistValidatorEvent. Failed to convert public key to BridgeAuthorityPublicKey: {:?}", e))
313        )?;
314        let new_url = String::from_utf8(event.new_url).map_err(|e|
315            BridgeError::Generic(format!("Failed to convert MoveBlocklistValidatorEvent to BlocklistValidatorEvent. Failed to convert new_url to String: {:?}", e))
316        )?;
317        Ok(Self { member, new_url })
318    }
319}
320
321impl TryFrom<MoveTokenDepositedEvent> for EmittedSuiToEthTokenBridgeV1 {
322    type Error = BridgeError;
323
324    fn try_from(event: MoveTokenDepositedEvent) -> BridgeResult<Self> {
325        if event.amount_sui_adjusted == 0 {
326            return Err(BridgeError::ZeroValueBridgeTransfer(format!(
327                "Failed to convert MoveTokenDepositedEvent to EmittedSuiToEthTokenBridgeV1. Manual intervention is required. 0 value transfer should not be allowed in Move: {:?}",
328                event,
329            )));
330        }
331
332        let token_id = event.token_type;
333        let sui_chain_id = BridgeChainId::try_from(event.source_chain).map_err(|_e| {
334            BridgeError::Generic(format!(
335                "Failed to convert MoveTokenDepositedEvent to EmittedSuiToEthTokenBridgeV1. Failed to convert source chain {} to BridgeChainId",
336                event.token_type,
337            ))
338        })?;
339        let eth_chain_id = BridgeChainId::try_from(event.target_chain).map_err(|_e| {
340            BridgeError::Generic(format!(
341                "Failed to convert MoveTokenDepositedEvent to EmittedSuiToEthTokenBridgeV1. Failed to convert target chain {} to BridgeChainId",
342                event.token_type,
343            ))
344        })?;
345        if !sui_chain_id.is_sui_chain() {
346            return Err(BridgeError::Generic(format!(
347                "Failed to convert MoveTokenDepositedEvent to EmittedSuiToEthTokenBridgeV1. Invalid source chain {}",
348                event.source_chain
349            )));
350        }
351        if eth_chain_id.is_sui_chain() {
352            return Err(BridgeError::Generic(format!(
353                "Failed to convert MoveTokenDepositedEvent to EmittedSuiToEthTokenBridgeV1. Invalid target chain {}",
354                event.target_chain
355            )));
356        }
357
358        let sui_address = SuiAddress::from_bytes(event.sender_address)
359            .map_err(|e| BridgeError::Generic(format!("Failed to convert MoveTokenDepositedEvent to EmittedSuiToEthTokenBridgeV1. Failed to convert sender_address to SuiAddress: {:?}", e)))?;
360        let eth_address = EthAddress::from_str(&Hex::encode(&event.target_address))?;
361
362        Ok(Self {
363            nonce: event.seq_num,
364            sui_chain_id,
365            eth_chain_id,
366            sui_address,
367            eth_address,
368            token_id,
369            amount_sui_adjusted: event.amount_sui_adjusted,
370        })
371    }
372}
373
374impl TryFrom<MoveTokenDepositedEventV2> for EmittedSuiToEthTokenBridgeV2 {
375    type Error = BridgeError;
376
377    fn try_from(event: MoveTokenDepositedEventV2) -> BridgeResult<Self> {
378        if event.amount_sui_adjusted == 0 {
379            return Err(BridgeError::ZeroValueBridgeTransfer(format!(
380                "Failed to convert MoveTokenDepositedEvent to EmittedSuiToEthTokenBridgeV1. Manual intervention is required. 0 value transfer should not be allowed in Move: {:?}",
381                event,
382            )));
383        }
384
385        let token_id = event.token_type;
386        let sui_chain_id = BridgeChainId::try_from(event.source_chain).map_err(|_e| {
387            BridgeError::Generic(format!(
388                "Failed to convert MoveTokenDepositedEvent to EmittedSuiToEthTokenBridgeV1. Failed to convert source chain {} to BridgeChainId",
389                event.token_type,
390            ))
391        })?;
392        let eth_chain_id = BridgeChainId::try_from(event.target_chain).map_err(|_e| {
393            BridgeError::Generic(format!(
394                "Failed to convert MoveTokenDepositedEvent to EmittedSuiToEthTokenBridgeV1. Failed to convert target chain {} to BridgeChainId",
395                event.token_type,
396            ))
397        })?;
398        if !sui_chain_id.is_sui_chain() {
399            return Err(BridgeError::Generic(format!(
400                "Failed to convert MoveTokenDepositedEvent to EmittedSuiToEthTokenBridgeV1. Invalid source chain {}",
401                event.source_chain
402            )));
403        }
404        if eth_chain_id.is_sui_chain() {
405            return Err(BridgeError::Generic(format!(
406                "Failed to convert MoveTokenDepositedEvent to EmittedSuiToEthTokenBridgeV1. Invalid target chain {}",
407                event.target_chain
408            )));
409        }
410
411        let sui_address = SuiAddress::from_bytes(event.sender_address)
412            .map_err(|e| BridgeError::Generic(format!("Failed to convert MoveTokenDepositedEvent to EmittedSuiToEthTokenBridgeV1. Failed to convert sender_address to SuiAddress: {:?}", e)))?;
413        let eth_address = EthAddress::from_str(&Hex::encode(&event.target_address))?;
414
415        Ok(Self {
416            nonce: event.seq_num,
417            sui_chain_id,
418            eth_chain_id,
419            sui_address,
420            eth_address,
421            token_id,
422            amount_sui_adjusted: event.amount_sui_adjusted,
423            timestamp_ms: event.timestamp_ms,
424        })
425    }
426}
427
428crate::declare_events!(
429    SuiToEthTokenBridgeV1(EmittedSuiToEthTokenBridgeV1) => ("bridge::TokenDepositedEvent", MoveTokenDepositedEvent),
430    TokenTransferApproved(TokenTransferApproved) => ("bridge::TokenTransferApproved", MoveTokenTransferApproved),
431    TokenTransferClaimed(TokenTransferClaimed) => ("bridge::TokenTransferClaimed", MoveTokenTransferClaimed),
432    TokenTransferAlreadyApproved(TokenTransferAlreadyApproved) => ("bridge::TokenTransferAlreadyApproved", MoveTokenTransferAlreadyApproved),
433    TokenTransferAlreadyClaimed(TokenTransferAlreadyClaimed) => ("bridge::TokenTransferAlreadyClaimed", MoveTokenTransferAlreadyClaimed),
434    TokenTransferLimitExceed(TokenTransferLimitExceed) => ("bridge::TokenTransferLimitExceed", MoveTokenTransferLimitExceed),
435    EmergencyOpEvent(EmergencyOpEvent) => ("bridge::EmergencyOpEvent", EmergencyOpEvent),
436    // No need to define a sanitized event struct for MoveTypeCommitteeMemberRegistration
437    // because the info provided by validators could be invalid
438    CommitteeMemberRegistration(MoveTypeCommitteeMemberRegistration) => ("committee::CommitteeMemberRegistration", MoveTypeCommitteeMemberRegistration),
439    CommitteeUpdateEvent(CommitteeUpdate) => ("committee::CommitteeUpdateEvent", MoveCommitteeUpdateEvent),
440    CommitteeMemberUrlUpdateEvent(CommitteeMemberUrlUpdateEvent) => ("committee::CommitteeMemberUrlUpdateEvent", MoveCommitteeMemberUrlUpdateEvent),
441    BlocklistValidatorEvent(BlocklistValidatorEvent) => ("committee::BlocklistValidatorEvent", MoveBlocklistValidatorEvent),
442    TokenRegistrationEvent(TokenRegistrationEvent) => ("treasury::TokenRegistrationEvent", MoveTokenRegistrationEvent),
443    NewTokenEvent(NewTokenEvent) => ("treasury::NewTokenEvent", MoveNewTokenEvent),
444    UpdateTokenPriceEvent(UpdateTokenPriceEvent) => ("treasury::UpdateTokenPriceEvent", UpdateTokenPriceEvent),
445    UpdateRouteLimitEvent(UpdateRouteLimitEvent) => ("limiter::UpdateRouteLimitEvent", UpdateRouteLimitEvent),
446    SuiToEthTokenBridgeV2(EmittedSuiToEthTokenBridgeV2) => ("bridge::TokenDepositedEventV2", MoveTokenDepositedEventV2),
447
448    // Add new event types here. Format:
449    // EnumVariantName(Struct) => ("{module}::{event_struct}", CorrespondingMoveStruct)
450);
451
452#[macro_export]
453macro_rules! declare_events {
454    ($($variant:ident($type:path) => ($event_tag:expr, $event_struct:path)),* $(,)?) => {
455
456        #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
457        pub enum SuiBridgeEvent {
458            $($variant($type),)*
459        }
460
461        $(pub static $variant: OnceCell<StructTag> = OnceCell::new();)*
462
463        pub(crate) fn init_all_struct_tags() {
464            $($variant.get_or_init(|| {
465                StructTag::from_str(&format!("0x{}::{}", BRIDGE_PACKAGE_ID.to_hex(), $event_tag)).unwrap()
466            });)*
467        }
468
469        // Try to convert a SuiEvent into SuiBridgeEvent
470        impl SuiBridgeEvent {
471            pub fn try_from_sui_event(event: &SuiEvent) -> BridgeResult<Option<SuiBridgeEvent>> {
472                init_all_struct_tags(); // Ensure all tags are initialized
473
474                // Unwrap safe: we inited above
475                $(
476                    if &event.type_ == $variant.get().unwrap() {
477                        let event_struct: $event_struct = bcs::from_bytes(event.bcs.bytes()).map_err(|e| BridgeError::InternalError(format!("Failed to deserialize event to {}: {:?}", stringify!($event_struct), e)))?;
478                        return Ok(Some(SuiBridgeEvent::$variant(event_struct.try_into()?)));
479                    }
480                )*
481                Ok(None)
482            }
483
484            pub fn try_from_event(event: &Event) -> BridgeResult<Option<SuiBridgeEvent>> {
485                init_all_struct_tags(); // Ensure all tags are initialized
486
487                if event.type_.address != BRIDGE_PACKAGE_ID.into() {
488                    return Ok(None);
489                }
490
491                // Unwrap safe: we inited above
492                $(
493                    if &event.type_ == $variant.get().unwrap() {
494                        let event_struct: $event_struct = bcs::from_bytes(&event.contents).map_err(|e| BridgeError::InternalError(format!("Failed to deserialize event to {}: {:?}", stringify!($event_struct), e)))?;
495                        return Ok(Some(SuiBridgeEvent::$variant(event_struct.try_into()?)));
496                    }
497                )*
498                Ok(None)
499            }
500        }
501    };
502}
503
504impl SuiBridgeEvent {
505    pub fn try_into_bridge_action(self) -> Option<BridgeAction> {
506        match self {
507            SuiBridgeEvent::SuiToEthTokenBridgeV1(event) => {
508                let EmittedSuiToEthTokenBridgeV1 {
509                    nonce,
510                    sui_chain_id,
511                    eth_chain_id,
512                    sui_address,
513                    eth_address,
514                    token_id,
515                    amount_sui_adjusted,
516                } = event;
517
518                Some(BridgeAction::SuiToEthTokenTransfer(SuiToEthTokenTransfer {
519                    nonce,
520                    sui_chain_id,
521                    eth_chain_id,
522                    sui_address,
523                    eth_address,
524                    token_id,
525                    amount_adjusted: amount_sui_adjusted,
526                }))
527            }
528            SuiBridgeEvent::SuiToEthTokenBridgeV2(event) => Some(
529                BridgeAction::SuiToEthTokenTransferV2(SuiToEthTokenTransferV2 {
530                    nonce: event.nonce,
531                    sui_chain_id: event.sui_chain_id,
532                    eth_chain_id: event.eth_chain_id,
533                    sui_address: event.sui_address,
534                    eth_address: event.eth_address,
535                    token_id: event.token_id,
536                    amount_adjusted: event.amount_sui_adjusted,
537                    timestamp_ms: event.timestamp_ms,
538                }),
539            ),
540            SuiBridgeEvent::TokenTransferApproved(_event) => None,
541            SuiBridgeEvent::TokenTransferClaimed(_event) => None,
542            SuiBridgeEvent::TokenTransferAlreadyApproved(_event) => None,
543            SuiBridgeEvent::TokenTransferAlreadyClaimed(_event) => None,
544            SuiBridgeEvent::TokenTransferLimitExceed(_event) => None,
545            SuiBridgeEvent::EmergencyOpEvent(_event) => None,
546            SuiBridgeEvent::CommitteeMemberRegistration(_event) => None,
547            SuiBridgeEvent::CommitteeUpdateEvent(_event) => None,
548            SuiBridgeEvent::CommitteeMemberUrlUpdateEvent(_event) => None,
549            SuiBridgeEvent::BlocklistValidatorEvent(_event) => None,
550            SuiBridgeEvent::TokenRegistrationEvent(_event) => None,
551            SuiBridgeEvent::NewTokenEvent(_event) => None,
552            SuiBridgeEvent::UpdateTokenPriceEvent(_event) => None,
553            SuiBridgeEvent::UpdateRouteLimitEvent(_event) => None,
554        }
555    }
556}
557
558#[cfg(test)]
559pub mod tests {
560    use std::collections::HashSet;
561
562    use super::*;
563    use crate::crypto::BridgeAuthorityKeyPair;
564    use crate::e2e_tests::test_utils::BridgeTestClusterBuilder;
565    use crate::types::BridgeAction;
566    use crate::types::SuiToEthBridgeAction;
567    use alloy::primitives::Address as EthAddress;
568    use sui_json_rpc_types::BcsEvent;
569    use sui_json_rpc_types::SuiEvent;
570    use sui_types::Identifier;
571    use sui_types::base_types::ObjectID;
572    use sui_types::base_types::SuiAddress;
573    use sui_types::bridge::BridgeChainId;
574    use sui_types::bridge::TOKEN_ID_SUI;
575    use sui_types::crypto::get_key_pair;
576    use sui_types::digests::TransactionDigest;
577    use sui_types::event::EventID;
578
579    /// Returns a test SuiEvent and corresponding BridgeAction
580    pub fn get_test_sui_event_and_action(identifier: Identifier) -> (SuiEvent, BridgeAction) {
581        init_all_struct_tags(); // Ensure all tags are initialized
582        let sanitized_event = EmittedSuiToEthTokenBridgeV1 {
583            nonce: 1,
584            sui_chain_id: BridgeChainId::SuiTestnet,
585            sui_address: SuiAddress::random_for_testing_only(),
586            eth_chain_id: BridgeChainId::EthSepolia,
587            eth_address: EthAddress::random(),
588            token_id: TOKEN_ID_SUI,
589            amount_sui_adjusted: 100,
590        };
591        let emitted_event = MoveTokenDepositedEvent {
592            seq_num: sanitized_event.nonce,
593            source_chain: sanitized_event.sui_chain_id as u8,
594            sender_address: sanitized_event.sui_address.to_vec(),
595            target_chain: sanitized_event.eth_chain_id as u8,
596            target_address: sanitized_event.eth_address.to_vec(),
597            token_type: sanitized_event.token_id,
598            amount_sui_adjusted: sanitized_event.amount_sui_adjusted,
599        };
600
601        let tx_digest = TransactionDigest::random();
602        let event_idx = 10u16;
603        let bridge_action = BridgeAction::SuiToEthBridgeAction(SuiToEthBridgeAction {
604            sui_tx_digest: tx_digest,
605            sui_tx_event_index: event_idx,
606            sui_bridge_event: sanitized_event.clone(),
607        });
608        let event = SuiEvent {
609            type_: SuiToEthTokenBridgeV1.get().unwrap().clone(),
610            bcs: BcsEvent::new(bcs::to_bytes(&emitted_event).unwrap()),
611            id: EventID {
612                tx_digest,
613                event_seq: event_idx as u64,
614            },
615
616            // The following fields do not matter as of writing,
617            // but if tests start to fail, it's worth checking these fields.
618            package_id: ObjectID::ZERO,
619            transaction_module: identifier.clone(),
620            sender: SuiAddress::random_for_testing_only(),
621            parsed_json: serde_json::json!({"test": "test"}),
622            timestamp_ms: None,
623        };
624        (event, bridge_action)
625    }
626
627    #[tokio::test(flavor = "multi_thread", worker_threads = 8)]
628    async fn test_bridge_events_when_init() {
629        telemetry_subscribers::init_for_testing();
630        init_all_struct_tags();
631        let mut bridge_test_cluster = BridgeTestClusterBuilder::new()
632            .with_eth_env(false)
633            .with_bridge_cluster(false)
634            .with_num_validators(2)
635            .build()
636            .await;
637
638        let events = bridge_test_cluster
639            .new_bridge_events(HashSet::from_iter([
640                CommitteeMemberRegistration.get().unwrap().clone(),
641                CommitteeUpdateEvent.get().unwrap().clone(),
642                TokenRegistrationEvent.get().unwrap().clone(),
643                NewTokenEvent.get().unwrap().clone(),
644            ]))
645            .await;
646        let mut mask = 0u8;
647        for event in events.iter() {
648            match SuiBridgeEvent::try_from_event(event).unwrap().unwrap() {
649                SuiBridgeEvent::CommitteeMemberRegistration(_event) => mask |= 0x1,
650                SuiBridgeEvent::CommitteeUpdateEvent(_event) => mask |= 0x2,
651                SuiBridgeEvent::TokenRegistrationEvent(_event) => mask |= 0x4,
652                SuiBridgeEvent::NewTokenEvent(_event) => mask |= 0x8,
653                _ => panic!("Got unexpected event: {:?}", event),
654            }
655        }
656        // assert all the above events are emitted
657        assert_eq!(mask, 0xF);
658
659        // TODO: trigger other events and make sure they are converted correctly
660    }
661
662    #[test]
663    fn test_conversion_for_committee_member_url_update_event() {
664        let (_, kp): (_, BridgeAuthorityKeyPair) = get_key_pair();
665        let new_url = "https://example.com:443";
666        let event: CommitteeMemberUrlUpdateEvent = MoveCommitteeMemberUrlUpdateEvent {
667            member: kp.public.as_bytes().to_vec(),
668            new_url: new_url.as_bytes().to_vec(),
669        }
670        .try_into()
671        .unwrap();
672        assert_eq!(event.member, kp.public);
673        assert_eq!(event.new_url, new_url);
674
675        CommitteeMemberUrlUpdateEvent::try_from(MoveCommitteeMemberUrlUpdateEvent {
676            member: vec![1, 2, 3],
677            new_url: new_url.as_bytes().to_vec(),
678        })
679        .unwrap_err();
680
681        CommitteeMemberUrlUpdateEvent::try_from(MoveCommitteeMemberUrlUpdateEvent {
682            member: kp.public.as_bytes().to_vec(),
683            new_url: [240, 130, 130, 172].into(),
684        })
685        .unwrap_err();
686    }
687
688    // TODO: add conversion tests for other events
689
690    #[test]
691    fn test_0_sui_amount_conversion_for_sui_event() {
692        let emitted_event = MoveTokenDepositedEvent {
693            seq_num: 1,
694            source_chain: BridgeChainId::SuiTestnet as u8,
695            sender_address: SuiAddress::random_for_testing_only().to_vec(),
696            target_chain: BridgeChainId::EthSepolia as u8,
697            target_address: EthAddress::random().to_vec(),
698            token_type: TOKEN_ID_SUI,
699            amount_sui_adjusted: 0,
700        };
701        match EmittedSuiToEthTokenBridgeV1::try_from(emitted_event).unwrap_err() {
702            BridgeError::ZeroValueBridgeTransfer(_) => (),
703            other => panic!("Expected Generic error, got: {:?}", other),
704        }
705    }
706}