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