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