1use anyhow::anyhow;
5use async_trait::async_trait;
6use core::panic;
7use fastcrypto::traits::ToFromBytes;
8use serde::de::DeserializeOwned;
9use std::collections::HashMap;
10use std::str::from_utf8;
11use std::sync::Arc;
12use std::time::Duration;
13use sui_json_rpc_api::BridgeReadApiClient;
14use sui_json_rpc_types::DevInspectResults;
15use sui_json_rpc_types::{EventFilter, Page, SuiEvent};
16use sui_json_rpc_types::{
17    EventPage, SuiObjectDataOptions, SuiTransactionBlockResponse,
18    SuiTransactionBlockResponseOptions,
19};
20use sui_sdk::{SuiClient as SuiSdkClient, SuiClientBuilder};
21use sui_types::BRIDGE_PACKAGE_ID;
22use sui_types::SUI_BRIDGE_OBJECT_ID;
23use sui_types::TypeTag;
24use sui_types::base_types::ObjectRef;
25use sui_types::base_types::SequenceNumber;
26use sui_types::bridge::BridgeSummary;
27use sui_types::bridge::BridgeTreasurySummary;
28use sui_types::bridge::MoveTypeCommitteeMember;
29use sui_types::bridge::MoveTypeParsedTokenTransferMessage;
30use sui_types::gas_coin::GasCoin;
31use sui_types::object::Owner;
32use sui_types::parse_sui_type_tag;
33use sui_types::transaction::Argument;
34use sui_types::transaction::CallArg;
35use sui_types::transaction::Command;
36use sui_types::transaction::ObjectArg;
37use sui_types::transaction::ProgrammableTransaction;
38use sui_types::transaction::SharedObjectMutability;
39use sui_types::transaction::Transaction;
40use sui_types::transaction::TransactionKind;
41use sui_types::{
42    Identifier,
43    base_types::{ObjectID, SuiAddress},
44    digests::TransactionDigest,
45    event::EventID,
46};
47use tokio::sync::OnceCell;
48use tracing::{error, warn};
49
50use crate::crypto::BridgeAuthorityPublicKey;
51use crate::error::{BridgeError, BridgeResult};
52use crate::events::SuiBridgeEvent;
53use crate::metrics::BridgeMetrics;
54use crate::retry_with_max_elapsed_time;
55use crate::types::BridgeActionStatus;
56use crate::types::ParsedTokenTransferMessage;
57use crate::types::{BridgeAction, BridgeAuthority, BridgeCommittee};
58
59pub struct SuiClient<P> {
60    inner: P,
61    bridge_metrics: Arc<BridgeMetrics>,
62}
63
64pub type SuiBridgeClient = SuiClient<SuiSdkClient>;
65
66impl SuiBridgeClient {
67    pub async fn new(rpc_url: &str, bridge_metrics: Arc<BridgeMetrics>) -> anyhow::Result<Self> {
68        let inner = SuiClientBuilder::default()
69            .build(rpc_url)
70            .await
71            .map_err(|e| {
72                anyhow!("Can't establish connection with Sui Rpc {rpc_url}. Error: {e}")
73            })?;
74        let self_ = Self {
75            inner,
76            bridge_metrics,
77        };
78        self_.describe().await?;
79        Ok(self_)
80    }
81
82    pub fn sui_client(&self) -> &SuiSdkClient {
83        &self.inner
84    }
85}
86
87impl<P> SuiClient<P>
88where
89    P: SuiClientInner,
90{
91    pub fn new_for_testing(inner: P) -> Self {
92        Self {
93            inner,
94            bridge_metrics: Arc::new(BridgeMetrics::new_for_testing()),
95        }
96    }
97
98    async fn describe(&self) -> anyhow::Result<()> {
100        let chain_id = self.inner.get_chain_identifier().await?;
101        let block_number = self.inner.get_latest_checkpoint_sequence_number().await?;
102        tracing::info!(
103            "SuiClient is connected to chain {chain_id}, current block number: {block_number}"
104        );
105        Ok(())
106    }
107
108    pub async fn get_mutable_bridge_object_arg_must_succeed(&self) -> ObjectArg {
113        static ARG: OnceCell<ObjectArg> = OnceCell::const_new();
114        *ARG.get_or_init(|| async move {
115            let Ok(Ok(bridge_object_arg)) = retry_with_max_elapsed_time!(
116                self.inner.get_mutable_bridge_object_arg(),
117                Duration::from_secs(30)
118            ) else {
119                panic!("Failed to get bridge object arg after retries");
120            };
121            bridge_object_arg
122        })
123        .await
124    }
125
126    pub async fn query_events_by_module(
128        &self,
129        package: ObjectID,
130        module: Identifier,
131        cursor: Option<EventID>,
133    ) -> BridgeResult<Page<SuiEvent, EventID>> {
134        let filter = EventFilter::MoveEventModule {
135            package,
136            module: module.clone(),
137        };
138        let events = self.inner.query_events(filter.clone(), cursor).await?;
139
140        assert!(
142            events
143                .data
144                .iter()
145                .all(|event| event.type_.address.as_ref() == package.as_ref()
146                    && event.type_.module == module)
147        );
148        Ok(events)
149    }
150
151    pub async fn get_bridge_action_by_tx_digest_and_event_idx_maybe(
155        &self,
156        tx_digest: &TransactionDigest,
157        event_idx: u16,
158    ) -> BridgeResult<BridgeAction> {
159        let events = self.inner.get_events_by_tx_digest(*tx_digest).await?;
160        let event = events
161            .get(event_idx as usize)
162            .ok_or(BridgeError::NoBridgeEventsInTxPosition)?;
163        if event.type_.address.as_ref() != BRIDGE_PACKAGE_ID.as_ref() {
164            return Err(BridgeError::BridgeEventInUnrecognizedSuiPackage);
165        }
166        let bridge_event = SuiBridgeEvent::try_from_sui_event(event)?
167            .ok_or(BridgeError::NoBridgeEventsInTxPosition)?;
168
169        bridge_event
170            .try_into_bridge_action(*tx_digest, event_idx)
171            .ok_or(BridgeError::BridgeEventNotActionable)
172    }
173
174    pub async fn get_bridge_summary(&self) -> BridgeResult<BridgeSummary> {
175        self.inner
176            .get_bridge_summary()
177            .await
178            .map_err(|e| BridgeError::InternalError(format!("Can't get bridge committee: {e}")))
179    }
180
181    pub async fn is_bridge_paused(&self) -> BridgeResult<bool> {
182        self.get_bridge_summary()
183            .await
184            .map(|summary| summary.is_frozen)
185    }
186
187    pub async fn get_treasury_summary(&self) -> BridgeResult<BridgeTreasurySummary> {
188        Ok(self.get_bridge_summary().await?.treasury)
189    }
190
191    pub async fn get_token_id_map(&self) -> BridgeResult<HashMap<u8, TypeTag>> {
192        self.get_bridge_summary()
193            .await?
194            .treasury
195            .id_token_type_map
196            .into_iter()
197            .map(|(id, name)| {
198                parse_sui_type_tag(&format!("0x{name}"))
199                    .map(|name| (id, name))
200                    .map_err(|e| {
201                        BridgeError::InternalError(format!(
202                            "Failed to retrieve token id mapping: {e}, type name: {name}"
203                        ))
204                    })
205            })
206            .collect()
207    }
208
209    pub async fn get_notional_values(&self) -> BridgeResult<HashMap<u8, u64>> {
210        let bridge_summary = self.get_bridge_summary().await?;
211        bridge_summary
212            .treasury
213            .id_token_type_map
214            .iter()
215            .map(|(id, type_name)| {
216                bridge_summary
217                    .treasury
218                    .supported_tokens
219                    .iter()
220                    .find_map(|(tn, metadata)| {
221                        if type_name == tn {
222                            Some((*id, metadata.notional_value))
223                        } else {
224                            None
225                        }
226                    })
227                    .ok_or(BridgeError::InternalError(
228                        "Error encountered when retrieving token notional values.".into(),
229                    ))
230            })
231            .collect()
232    }
233
234    pub async fn get_bridge_committee(&self) -> BridgeResult<BridgeCommittee> {
235        let bridge_summary =
236            self.inner.get_bridge_summary().await.map_err(|e| {
237                BridgeError::InternalError(format!("Can't get bridge committee: {e}"))
238            })?;
239        let move_type_bridge_committee = bridge_summary.committee;
240
241        let mut authorities = vec![];
242        for (_, member) in move_type_bridge_committee.members {
244            let MoveTypeCommitteeMember {
245                sui_address,
246                bridge_pubkey_bytes,
247                voting_power,
248                http_rest_url,
249                blocklisted,
250            } = member;
251            let pubkey = BridgeAuthorityPublicKey::from_bytes(&bridge_pubkey_bytes)?;
252            let base_url = from_utf8(&http_rest_url).unwrap_or_else(|_e| {
253                warn!(
254                    "Bridge authority address: {}, pubkey: {:?} has invalid http url: {:?}",
255                    sui_address, bridge_pubkey_bytes, http_rest_url
256                );
257                ""
258            });
259            authorities.push(BridgeAuthority {
260                sui_address,
261                pubkey,
262                voting_power,
263                base_url: base_url.into(),
264                is_blocklisted: blocklisted,
265            });
266        }
267        BridgeCommittee::new(authorities)
268    }
269
270    pub async fn get_chain_identifier(&self) -> BridgeResult<String> {
271        Ok(self.inner.get_chain_identifier().await?)
272    }
273
274    pub async fn get_reference_gas_price_until_success(&self) -> u64 {
275        loop {
276            let Ok(Ok(rgp)) = retry_with_max_elapsed_time!(
277                self.inner.get_reference_gas_price(),
278                Duration::from_secs(30)
279            ) else {
280                self.bridge_metrics
281                    .sui_rpc_errors
282                    .with_label_values(&["get_reference_gas_price"])
283                    .inc();
284                error!("Failed to get reference gas price");
285                continue;
286            };
287            return rgp;
288        }
289    }
290
291    pub async fn get_latest_checkpoint_sequence_number(&self) -> BridgeResult<u64> {
292        Ok(self.inner.get_latest_checkpoint_sequence_number().await?)
293    }
294
295    pub async fn execute_transaction_block_with_effects(
296        &self,
297        tx: sui_types::transaction::Transaction,
298    ) -> BridgeResult<SuiTransactionBlockResponse> {
299        self.inner.execute_transaction_block_with_effects(tx).await
300    }
301
302    pub async fn get_token_transfer_action_onchain_status_until_success(
304        &self,
305        source_chain_id: u8,
306        seq_number: u64,
307    ) -> BridgeActionStatus {
308        loop {
309            let bridge_object_arg = self.get_mutable_bridge_object_arg_must_succeed().await;
310            let Ok(Ok(status)) = retry_with_max_elapsed_time!(
311                self.inner.get_token_transfer_action_onchain_status(
312                    bridge_object_arg,
313                    source_chain_id,
314                    seq_number
315                ),
316                Duration::from_secs(30)
317            ) else {
318                self.bridge_metrics
319                    .sui_rpc_errors
320                    .with_label_values(&["get_token_transfer_action_onchain_status"])
321                    .inc();
322                error!(
323                    source_chain_id,
324                    seq_number, "Failed to get token transfer action onchain status"
325                );
326                continue;
327            };
328            return status;
329        }
330    }
331
332    pub async fn get_token_transfer_action_onchain_signatures_until_success(
333        &self,
334        source_chain_id: u8,
335        seq_number: u64,
336    ) -> Option<Vec<Vec<u8>>> {
337        loop {
338            let bridge_object_arg = self.get_mutable_bridge_object_arg_must_succeed().await;
339            let Ok(Ok(sigs)) = retry_with_max_elapsed_time!(
340                self.inner.get_token_transfer_action_onchain_signatures(
341                    bridge_object_arg,
342                    source_chain_id,
343                    seq_number
344                ),
345                Duration::from_secs(30)
346            ) else {
347                self.bridge_metrics
348                    .sui_rpc_errors
349                    .with_label_values(&["get_token_transfer_action_onchain_signatures"])
350                    .inc();
351                error!(
352                    source_chain_id,
353                    seq_number, "Failed to get token transfer action onchain signatures"
354                );
355                continue;
356            };
357            return sigs;
358        }
359    }
360
361    pub async fn get_parsed_token_transfer_message(
362        &self,
363        source_chain_id: u8,
364        seq_number: u64,
365    ) -> BridgeResult<Option<ParsedTokenTransferMessage>> {
366        let bridge_object_arg = self.get_mutable_bridge_object_arg_must_succeed().await;
367        let message = self
368            .inner
369            .get_parsed_token_transfer_message(bridge_object_arg, source_chain_id, seq_number)
370            .await?;
371        Ok(match message {
372            Some(payload) => Some(ParsedTokenTransferMessage::try_from(payload)?),
373            None => None,
374        })
375    }
376
377    pub async fn get_gas_data_panic_if_not_gas(
378        &self,
379        gas_object_id: ObjectID,
380    ) -> (GasCoin, ObjectRef, Owner) {
381        self.inner
382            .get_gas_data_panic_if_not_gas(gas_object_id)
383            .await
384    }
385}
386
387#[async_trait]
389pub trait SuiClientInner: Send + Sync {
390    type Error: Into<anyhow::Error> + Send + Sync + std::error::Error + 'static;
391    async fn query_events(
392        &self,
393        query: EventFilter,
394        cursor: Option<EventID>,
395    ) -> Result<EventPage, Self::Error>;
396
397    async fn get_events_by_tx_digest(
398        &self,
399        tx_digest: TransactionDigest,
400    ) -> Result<Vec<SuiEvent>, Self::Error>;
401
402    async fn get_chain_identifier(&self) -> Result<String, Self::Error>;
403
404    async fn get_reference_gas_price(&self) -> Result<u64, Self::Error>;
405
406    async fn get_latest_checkpoint_sequence_number(&self) -> Result<u64, Self::Error>;
407
408    async fn get_mutable_bridge_object_arg(&self) -> Result<ObjectArg, Self::Error>;
409
410    async fn get_bridge_summary(&self) -> Result<BridgeSummary, Self::Error>;
411
412    async fn execute_transaction_block_with_effects(
413        &self,
414        tx: Transaction,
415    ) -> Result<SuiTransactionBlockResponse, BridgeError>;
416
417    async fn get_token_transfer_action_onchain_status(
418        &self,
419        bridge_object_arg: ObjectArg,
420        source_chain_id: u8,
421        seq_number: u64,
422    ) -> Result<BridgeActionStatus, BridgeError>;
423
424    async fn get_token_transfer_action_onchain_signatures(
425        &self,
426        bridge_object_arg: ObjectArg,
427        source_chain_id: u8,
428        seq_number: u64,
429    ) -> Result<Option<Vec<Vec<u8>>>, BridgeError>;
430
431    async fn get_parsed_token_transfer_message(
432        &self,
433        bridge_object_arg: ObjectArg,
434        source_chain_id: u8,
435        seq_number: u64,
436    ) -> Result<Option<MoveTypeParsedTokenTransferMessage>, BridgeError>;
437
438    async fn get_gas_data_panic_if_not_gas(
439        &self,
440        gas_object_id: ObjectID,
441    ) -> (GasCoin, ObjectRef, Owner);
442}
443
444#[async_trait]
445impl SuiClientInner for SuiSdkClient {
446    type Error = sui_sdk::error::Error;
447
448    async fn query_events(
449        &self,
450        query: EventFilter,
451        cursor: Option<EventID>,
452    ) -> Result<EventPage, Self::Error> {
453        self.event_api()
454            .query_events(query, cursor, None, false)
455            .await
456    }
457
458    async fn get_events_by_tx_digest(
459        &self,
460        tx_digest: TransactionDigest,
461    ) -> Result<Vec<SuiEvent>, Self::Error> {
462        self.event_api().get_events(tx_digest).await
463    }
464
465    async fn get_chain_identifier(&self) -> Result<String, Self::Error> {
466        self.read_api().get_chain_identifier().await
467    }
468
469    async fn get_reference_gas_price(&self) -> Result<u64, Self::Error> {
470        self.governance_api().get_reference_gas_price().await
471    }
472
473    async fn get_latest_checkpoint_sequence_number(&self) -> Result<u64, Self::Error> {
474        self.read_api()
475            .get_latest_checkpoint_sequence_number()
476            .await
477    }
478
479    async fn get_mutable_bridge_object_arg(&self) -> Result<ObjectArg, Self::Error> {
480        let initial_shared_version = self
481            .http()
482            .get_bridge_object_initial_shared_version()
483            .await?;
484        Ok(ObjectArg::SharedObject {
485            id: SUI_BRIDGE_OBJECT_ID,
486            initial_shared_version: SequenceNumber::from_u64(initial_shared_version),
487            mutability: SharedObjectMutability::Mutable,
488        })
489    }
490
491    async fn get_bridge_summary(&self) -> Result<BridgeSummary, Self::Error> {
492        self.http().get_latest_bridge().await.map_err(|e| e.into())
493    }
494
495    async fn get_token_transfer_action_onchain_status(
496        &self,
497        bridge_object_arg: ObjectArg,
498        source_chain_id: u8,
499        seq_number: u64,
500    ) -> Result<BridgeActionStatus, BridgeError> {
501        dev_inspect_bridge::<u8>(
502            self,
503            bridge_object_arg,
504            source_chain_id,
505            seq_number,
506            "get_token_transfer_action_status",
507        )
508        .await
509        .and_then(|status_byte| BridgeActionStatus::try_from(status_byte).map_err(Into::into))
510    }
511
512    async fn get_token_transfer_action_onchain_signatures(
513        &self,
514        bridge_object_arg: ObjectArg,
515        source_chain_id: u8,
516        seq_number: u64,
517    ) -> Result<Option<Vec<Vec<u8>>>, BridgeError> {
518        dev_inspect_bridge::<Option<Vec<Vec<u8>>>>(
519            self,
520            bridge_object_arg,
521            source_chain_id,
522            seq_number,
523            "get_token_transfer_action_signatures",
524        )
525        .await
526    }
527
528    async fn execute_transaction_block_with_effects(
529        &self,
530        tx: Transaction,
531    ) -> Result<SuiTransactionBlockResponse, BridgeError> {
532        match self.quorum_driver_api().execute_transaction_block(
533            tx,
534            SuiTransactionBlockResponseOptions::new().with_effects().with_events(),
535            Some(sui_types::quorum_driver_types::ExecuteTransactionRequestType::WaitForEffectsCert),
536        ).await {
537            Ok(response) => Ok(response),
538            Err(e) => return Err(BridgeError::SuiTxFailureGeneric(e.to_string())),
539        }
540    }
541
542    async fn get_parsed_token_transfer_message(
543        &self,
544        bridge_object_arg: ObjectArg,
545        source_chain_id: u8,
546        seq_number: u64,
547    ) -> Result<Option<MoveTypeParsedTokenTransferMessage>, BridgeError> {
548        dev_inspect_bridge::<Option<MoveTypeParsedTokenTransferMessage>>(
549            self,
550            bridge_object_arg,
551            source_chain_id,
552            seq_number,
553            "get_parsed_token_transfer_message",
554        )
555        .await
556    }
557
558    async fn get_gas_data_panic_if_not_gas(
559        &self,
560        gas_object_id: ObjectID,
561    ) -> (GasCoin, ObjectRef, Owner) {
562        loop {
563            match self
564                .read_api()
565                .get_object_with_options(
566                    gas_object_id,
567                    SuiObjectDataOptions::default().with_owner().with_content(),
568                )
569                .await
570                .map(|resp| resp.data)
571            {
572                Ok(Some(gas_obj)) => {
573                    let owner = gas_obj.owner.clone().expect("Owner is requested");
574                    let gas_coin = GasCoin::try_from(&gas_obj)
575                        .unwrap_or_else(|err| panic!("{} is not a gas coin: {err}", gas_object_id));
576                    return (gas_coin, gas_obj.object_ref(), owner);
577                }
578                other => {
579                    warn!("Can't get gas object: {:?}: {:?}", gas_object_id, other);
580                    tokio::time::sleep(Duration::from_secs(5)).await;
581                }
582            }
583        }
584    }
585}
586
587async fn dev_inspect_bridge<T>(
591    sui_client: &SuiSdkClient,
592    bridge_object_arg: ObjectArg,
593    source_chain_id: u8,
594    seq_number: u64,
595    function_name: &str,
596) -> Result<T, BridgeError>
597where
598    T: DeserializeOwned,
599{
600    let pt = ProgrammableTransaction {
601        inputs: vec![
602            CallArg::Object(bridge_object_arg),
603            CallArg::Pure(bcs::to_bytes(&source_chain_id).unwrap()),
604            CallArg::Pure(bcs::to_bytes(&seq_number).unwrap()),
605        ],
606        commands: vec![Command::move_call(
607            BRIDGE_PACKAGE_ID,
608            Identifier::new("bridge").unwrap(),
609            Identifier::new(function_name).unwrap(),
610            vec![],
611            vec![Argument::Input(0), Argument::Input(1), Argument::Input(2)],
612        )],
613    };
614    let kind = TransactionKind::programmable(pt);
615    let resp = sui_client
616        .read_api()
617        .dev_inspect_transaction_block(SuiAddress::ZERO, kind, None, None, None)
618        .await?;
619    let DevInspectResults {
620        results, effects, ..
621    } = resp;
622    let Some(results) = results else {
623        return Err(BridgeError::Generic(format!(
624            "No results returned for '{}', effects: {:?}",
625            function_name, effects
626        )));
627    };
628    let return_values = &results
629        .first()
630        .ok_or(BridgeError::Generic(format!(
631            "No return values for '{}', results: {:?}",
632            function_name, results
633        )))?
634        .return_values;
635    let (value_bytes, _type_tag) = return_values.first().ok_or(BridgeError::Generic(format!(
636        "No first return value for '{}', results: {:?}",
637        function_name, results
638    )))?;
639    bcs::from_bytes::<T>(value_bytes).map_err(|e| {
640        BridgeError::Generic(format!(
641            "Failed to parse return value for '{}', error: {:?}, results: {:?}",
642            function_name, e, results
643        ))
644    })
645}
646
647#[cfg(test)]
648mod tests {
649    use crate::crypto::BridgeAuthorityKeyPair;
650    use crate::e2e_tests::test_utils::TestClusterWrapperBuilder;
651    use crate::{
652        events::{EmittedSuiToEthTokenBridgeV1, MoveTokenDepositedEvent},
653        sui_mock_client::SuiMockClient,
654        test_utils::{
655            approve_action_with_validator_secrets, bridge_token, get_test_eth_to_sui_bridge_action,
656            get_test_sui_to_eth_bridge_action,
657        },
658        types::SuiToEthBridgeAction,
659    };
660    use ethers::types::Address as EthAddress;
661    use move_core_types::account_address::AccountAddress;
662    use serde::{Deserialize, Serialize};
663    use std::str::FromStr;
664    use sui_json_rpc_types::BcsEvent;
665    use sui_types::bridge::{BridgeChainId, TOKEN_ID_SUI, TOKEN_ID_USDC};
666    use sui_types::crypto::get_key_pair;
667
668    use super::*;
669    use crate::events::{SuiToEthTokenBridgeV1, init_all_struct_tags};
670
671    #[tokio::test]
672    async fn get_bridge_action_by_tx_digest_and_event_idx_maybe() {
673        telemetry_subscribers::init_for_testing();
677        let mock_client = SuiMockClient::default();
678        let sui_client = SuiClient::new_for_testing(mock_client.clone());
679        let tx_digest = TransactionDigest::random();
680
681        init_all_struct_tags();
683
684        let sanitized_event_1 = EmittedSuiToEthTokenBridgeV1 {
685            nonce: 1,
686            sui_chain_id: BridgeChainId::SuiTestnet,
687            sui_address: SuiAddress::random_for_testing_only(),
688            eth_chain_id: BridgeChainId::EthSepolia,
689            eth_address: EthAddress::random(),
690            token_id: TOKEN_ID_SUI,
691            amount_sui_adjusted: 100,
692        };
693        let emitted_event_1 = MoveTokenDepositedEvent {
694            seq_num: sanitized_event_1.nonce,
695            source_chain: sanitized_event_1.sui_chain_id as u8,
696            sender_address: sanitized_event_1.sui_address.to_vec(),
697            target_chain: sanitized_event_1.eth_chain_id as u8,
698            target_address: sanitized_event_1.eth_address.as_bytes().to_vec(),
699            token_type: sanitized_event_1.token_id,
700            amount_sui_adjusted: sanitized_event_1.amount_sui_adjusted,
701        };
702
703        let mut sui_event_1 = SuiEvent::random_for_testing();
704        sui_event_1.type_ = SuiToEthTokenBridgeV1.get().unwrap().clone();
705        sui_event_1.bcs = BcsEvent::new(bcs::to_bytes(&emitted_event_1).unwrap());
706
707        #[derive(Serialize, Deserialize)]
708        struct RandomStruct {}
709
710        let event_2: RandomStruct = RandomStruct {};
711        let mut sui_event_2 = SuiEvent::random_for_testing();
713        sui_event_2.type_ = SuiToEthTokenBridgeV1.get().unwrap().clone();
714        sui_event_2.type_.module = Identifier::from_str("unrecognized_module").unwrap();
715        sui_event_2.bcs = BcsEvent::new(bcs::to_bytes(&event_2).unwrap());
716
717        let mut sui_event_3 = sui_event_1.clone();
719        sui_event_3.type_.address = AccountAddress::random();
720
721        mock_client.add_events_by_tx_digest(
722            tx_digest,
723            vec![
724                sui_event_1.clone(),
725                sui_event_2.clone(),
726                sui_event_1.clone(),
727                sui_event_3.clone(),
728            ],
729        );
730        let expected_action_1 = BridgeAction::SuiToEthBridgeAction(SuiToEthBridgeAction {
731            sui_tx_digest: tx_digest,
732            sui_tx_event_index: 0,
733            sui_bridge_event: sanitized_event_1.clone(),
734        });
735        assert_eq!(
736            sui_client
737                .get_bridge_action_by_tx_digest_and_event_idx_maybe(&tx_digest, 0)
738                .await
739                .unwrap(),
740            expected_action_1,
741        );
742        let expected_action_2 = BridgeAction::SuiToEthBridgeAction(SuiToEthBridgeAction {
743            sui_tx_digest: tx_digest,
744            sui_tx_event_index: 2,
745            sui_bridge_event: sanitized_event_1.clone(),
746        });
747        assert_eq!(
748            sui_client
749                .get_bridge_action_by_tx_digest_and_event_idx_maybe(&tx_digest, 2)
750                .await
751                .unwrap(),
752            expected_action_2,
753        );
754        assert!(matches!(
755            sui_client
756                .get_bridge_action_by_tx_digest_and_event_idx_maybe(&tx_digest, 1)
757                .await
758                .unwrap_err(),
759            BridgeError::NoBridgeEventsInTxPosition
760        ),);
761        assert!(matches!(
762            sui_client
763                .get_bridge_action_by_tx_digest_and_event_idx_maybe(&tx_digest, 3)
764                .await
765                .unwrap_err(),
766            BridgeError::BridgeEventInUnrecognizedSuiPackage
767        ),);
768        assert!(matches!(
769            sui_client
770                .get_bridge_action_by_tx_digest_and_event_idx_maybe(&tx_digest, 4)
771                .await
772                .unwrap_err(),
773            BridgeError::NoBridgeEventsInTxPosition
774        ),);
775
776        sui_event_2.type_ = SuiToEthTokenBridgeV1.get().unwrap().clone();
778        mock_client.add_events_by_tx_digest(tx_digest, vec![sui_event_2]);
779        sui_client
780            .get_bridge_action_by_tx_digest_and_event_idx_maybe(&tx_digest, 2)
781            .await
782            .unwrap_err();
783    }
784
785    #[tokio::test(flavor = "multi_thread", worker_threads = 8)]
789    async fn test_get_action_onchain_status_for_sui_to_eth_transfer() {
790        telemetry_subscribers::init_for_testing();
791        let mut bridge_keys = vec![];
792        for _ in 0..=3 {
793            let (_, kp): (_, BridgeAuthorityKeyPair) = get_key_pair();
794            bridge_keys.push(kp);
795        }
796        let mut test_cluster = TestClusterWrapperBuilder::new()
797            .with_bridge_authority_keys(bridge_keys)
798            .with_deploy_tokens(true)
799            .build()
800            .await;
801
802        let bridge_metrics = Arc::new(BridgeMetrics::new_for_testing());
803        let sui_client =
804            SuiClient::new(&test_cluster.inner.fullnode_handle.rpc_url, bridge_metrics)
805                .await
806                .unwrap();
807        let bridge_authority_keys = test_cluster.authority_keys_clone();
808
809        test_cluster
811            .trigger_reconfiguration_if_not_yet_and_assert_bridge_committee_initialized()
812            .await;
813        let context = &mut test_cluster.inner.wallet;
814        let sender = context.active_address().unwrap();
815        let usdc_amount = 5000000;
816        let bridge_object_arg = sui_client
817            .get_mutable_bridge_object_arg_must_succeed()
818            .await;
819        let id_token_map = sui_client.get_token_id_map().await.unwrap();
820
821        let action = get_test_eth_to_sui_bridge_action(None, Some(usdc_amount), Some(sender), None);
823        let usdc_object_ref = approve_action_with_validator_secrets(
824            context,
825            bridge_object_arg,
826            action.clone(),
827            &bridge_authority_keys,
828            Some(sender),
829            &id_token_map,
830        )
831        .await
832        .unwrap();
833
834        let status = sui_client
835            .inner
836            .get_token_transfer_action_onchain_status(
837                bridge_object_arg,
838                action.chain_id() as u8,
839                action.seq_number(),
840            )
841            .await
842            .unwrap();
843        assert_eq!(status, BridgeActionStatus::Claimed);
844
845        let eth_recv_address = EthAddress::random();
848        let bridge_event = bridge_token(
849            context,
850            eth_recv_address,
851            usdc_object_ref,
852            id_token_map.get(&TOKEN_ID_USDC).unwrap().clone(),
853            bridge_object_arg,
854        )
855        .await;
856        assert_eq!(bridge_event.nonce, 0);
857        assert_eq!(bridge_event.sui_chain_id, BridgeChainId::SuiCustom);
858        assert_eq!(bridge_event.eth_chain_id, BridgeChainId::EthCustom);
859        assert_eq!(bridge_event.eth_address, eth_recv_address);
860        assert_eq!(bridge_event.sui_address, sender);
861        assert_eq!(bridge_event.token_id, TOKEN_ID_USDC);
862        assert_eq!(bridge_event.amount_sui_adjusted, usdc_amount);
863
864        let action = get_test_sui_to_eth_bridge_action(
865            None,
866            None,
867            Some(bridge_event.nonce),
868            Some(bridge_event.amount_sui_adjusted),
869            Some(bridge_event.sui_address),
870            Some(bridge_event.eth_address),
871            Some(TOKEN_ID_USDC),
872        );
873        let status = sui_client
874            .inner
875            .get_token_transfer_action_onchain_status(
876                bridge_object_arg,
877                action.chain_id() as u8,
878                action.seq_number(),
879            )
880            .await
881            .unwrap();
882        assert_eq!(status, BridgeActionStatus::Pending);
884
885        approve_action_with_validator_secrets(
887            context,
888            bridge_object_arg,
889            action.clone(),
890            &bridge_authority_keys,
891            None,
892            &id_token_map,
893        )
894        .await;
895
896        let status = sui_client
897            .inner
898            .get_token_transfer_action_onchain_status(
899                bridge_object_arg,
900                action.chain_id() as u8,
901                action.seq_number(),
902            )
903            .await
904            .unwrap();
905        assert_eq!(status, BridgeActionStatus::Approved);
906
907        let action =
909            get_test_sui_to_eth_bridge_action(None, None, Some(100), None, None, None, None);
910        let status = sui_client
911            .inner
912            .get_token_transfer_action_onchain_status(
913                bridge_object_arg,
914                action.chain_id() as u8,
915                action.seq_number(),
916            )
917            .await
918            .unwrap();
919        assert_eq!(status, BridgeActionStatus::NotFound);
920    }
921}