sui_bridge/
sui_client.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::crypto::BridgeAuthorityPublicKey;
5use crate::error::{BridgeError, BridgeResult};
6use crate::events::SuiBridgeEvent;
7use crate::metrics::BridgeMetrics;
8use crate::retry_with_max_elapsed_time;
9use crate::types::BridgeActionStatus;
10use crate::types::ParsedTokenTransferMessage;
11use crate::types::SuiEvents;
12use crate::types::{BridgeAction, BridgeAuthority, BridgeCommittee};
13use anyhow::anyhow;
14use async_trait::async_trait;
15use core::panic;
16use fastcrypto::traits::ToFromBytes;
17use std::collections::HashMap;
18use std::str::from_utf8;
19use std::sync::Arc;
20use std::time::Duration;
21use sui_json_rpc_types::BcsEvent;
22use sui_json_rpc_types::{EventFilter, Page, SuiEvent};
23use sui_json_rpc_types::{
24    EventPage, SuiExecutionStatus, SuiObjectDataOptions, SuiTransactionBlockResponseOptions,
25};
26use sui_rpc::field::{FieldMask, FieldMaskUtil};
27use sui_rpc::proto::sui::rpc::v2::{
28    Checkpoint, ExecuteTransactionRequest, ExecutedTransaction, GetCheckpointRequest,
29    GetObjectRequest, GetServiceInfoRequest, GetTransactionRequest, Object,
30    Transaction as ProtoTransaction, UserSignature as ProtoUserSignature,
31};
32use sui_sdk::{SuiClient as SuiSdkClient, SuiClientBuilder};
33use sui_sdk_types::Address;
34use sui_types::BRIDGE_PACKAGE_ID;
35use sui_types::Identifier;
36use sui_types::SUI_BRIDGE_OBJECT_ID;
37use sui_types::TypeTag;
38use sui_types::base_types::ObjectID;
39use sui_types::base_types::ObjectRef;
40use sui_types::base_types::SequenceNumber;
41use sui_types::bridge::{
42    BridgeSummary, BridgeWrapper, MoveTypeBridgeMessageKey, MoveTypeBridgeRecord,
43};
44use sui_types::bridge::{BridgeTrait, BridgeTreasurySummary};
45use sui_types::bridge::{MoveTypeBridgeMessage, MoveTypeParsedTokenTransferMessage};
46use sui_types::bridge::{
47    MoveTypeCommitteeMember, MoveTypeTokenTransferPayload, MoveTypeTokenTransferPayloadV2,
48};
49use sui_types::collection_types::LinkedTableNode;
50use sui_types::digests::TransactionDigest;
51use sui_types::event::EventID;
52use sui_types::gas_coin::GasCoin;
53use sui_types::object::Owner;
54use sui_types::parse_sui_type_tag;
55use sui_types::transaction::ObjectArg;
56use sui_types::transaction::SharedObjectMutability;
57use sui_types::transaction::Transaction;
58use tokio::sync::OnceCell;
59use tracing::{error, warn};
60
61pub struct SuiClient<P> {
62    inner: P,
63    bridge_metrics: Arc<BridgeMetrics>,
64}
65
66pub type SuiBridgeClient = SuiClient<SuiClientInternal>;
67
68pub struct SuiClientInternal {
69    jsonrpc_client: SuiSdkClient,
70    grpc_client: sui_rpc::Client,
71}
72
73#[derive(Clone, Debug)]
74pub struct ExecuteTransactionResult {
75    pub status: SuiExecutionStatus,
76    pub events: Vec<SuiEvent>,
77}
78
79impl SuiBridgeClient {
80    pub async fn new(rpc_url: &str, bridge_metrics: Arc<BridgeMetrics>) -> anyhow::Result<Self> {
81        let jsonrpc_client = SuiClientBuilder::default()
82            .build(rpc_url)
83            .await
84            .map_err(|e| {
85                anyhow!("Can't establish connection with Sui Rpc {rpc_url}. Error: {e}")
86            })?;
87        let grpc_client = sui_rpc::Client::new(rpc_url)?;
88        let inner = SuiClientInternal {
89            jsonrpc_client,
90            grpc_client,
91        };
92        let self_ = Self {
93            inner,
94            bridge_metrics,
95        };
96        self_.describe().await.map_err(|e| anyhow::anyhow!("{e}"))?;
97        Ok(self_)
98    }
99
100    pub fn jsonrpc_client(&self) -> &SuiSdkClient {
101        &self.inner.jsonrpc_client
102    }
103
104    pub fn grpc_client(&self) -> &sui_rpc::Client {
105        &self.inner.grpc_client
106    }
107}
108
109impl<P> SuiClient<P>
110where
111    P: SuiClientInner,
112{
113    pub fn new_for_testing(inner: P) -> Self {
114        Self {
115            inner,
116            bridge_metrics: Arc::new(BridgeMetrics::new_for_testing()),
117        }
118    }
119
120    // TODO assert chain identifier
121    async fn describe(&self) -> Result<(), BridgeError> {
122        let chain_id = self.inner.get_chain_identifier().await?;
123        let block_number = self.inner.get_latest_checkpoint_sequence_number().await?;
124        tracing::info!(
125            "SuiClient is connected to chain {chain_id}, current block number: {block_number}"
126        );
127        Ok(())
128    }
129
130    /// Get the mutable bridge object arg on chain.
131    // We retry a few times in case of errors. If it fails eventually, we panic.
132    // In general it's safe to call in the beginning of the program.
133    // After the first call, the result is cached since the value should never change.
134    pub async fn get_mutable_bridge_object_arg_must_succeed(&self) -> ObjectArg {
135        static ARG: OnceCell<ObjectArg> = OnceCell::const_new();
136        *ARG.get_or_init(|| async move {
137            let Ok(Ok(bridge_object_arg)) = retry_with_max_elapsed_time!(
138                self.inner.get_mutable_bridge_object_arg(),
139                Duration::from_secs(30)
140            ) else {
141                panic!("Failed to get bridge object arg after retries");
142            };
143            bridge_object_arg
144        })
145        .await
146    }
147
148    /// Query emitted Events that are defined in the given Move Module.
149    pub async fn query_events_by_module(
150        &self,
151        package: ObjectID,
152        module: Identifier,
153        // cursor is exclusive
154        cursor: Option<EventID>,
155    ) -> BridgeResult<Page<SuiEvent, EventID>> {
156        let filter = EventFilter::MoveEventModule {
157            package,
158            module: module.clone(),
159        };
160        let events = self.inner.query_events(filter.clone(), cursor).await?;
161
162        // Safeguard check that all events are emitted from requested package and module
163        assert!(
164            events
165                .data
166                .iter()
167                .all(|event| event.type_.address.as_ref() == package.as_ref()
168                    && event.type_.module == module)
169        );
170        Ok(events)
171    }
172
173    /// Returns BridgeAction from a Sui Transaction with transaction hash
174    /// and the event index. If event is declared in an unrecognized
175    /// package, return error.
176    pub async fn get_bridge_action_by_tx_digest_and_event_idx_maybe(
177        &self,
178        tx_digest: &TransactionDigest,
179        event_idx: u16,
180    ) -> BridgeResult<BridgeAction> {
181        let events = self.inner.get_events_by_tx_digest(*tx_digest).await?;
182        let event = events
183            .events
184            .get(event_idx as usize)
185            .ok_or(BridgeError::NoBridgeEventsInTxPosition)?;
186        if event.type_.address.as_ref() != BRIDGE_PACKAGE_ID.as_ref() {
187            return Err(BridgeError::BridgeEventInUnrecognizedSuiPackage);
188        }
189        let bridge_event = SuiBridgeEvent::try_from_sui_event(event)?
190            .ok_or(BridgeError::NoBridgeEventsInTxPosition)?;
191
192        bridge_event
193            .try_into_bridge_action()
194            .ok_or(BridgeError::BridgeEventNotActionable)
195    }
196
197    pub async fn get_bridge_summary(&self) -> BridgeResult<BridgeSummary> {
198        self.inner.get_bridge_summary().await
199    }
200
201    pub async fn is_bridge_paused(&self) -> BridgeResult<bool> {
202        self.get_bridge_summary()
203            .await
204            .map(|summary| summary.is_frozen)
205    }
206
207    pub async fn get_treasury_summary(&self) -> BridgeResult<BridgeTreasurySummary> {
208        Ok(self.get_bridge_summary().await?.treasury)
209    }
210
211    pub async fn get_token_id_map(&self) -> BridgeResult<HashMap<u8, TypeTag>> {
212        self.get_bridge_summary()
213            .await?
214            .treasury
215            .id_token_type_map
216            .into_iter()
217            .map(|(id, name)| {
218                parse_sui_type_tag(&format!("0x{name}"))
219                    .map(|name| (id, name))
220                    .map_err(|e| {
221                        BridgeError::InternalError(format!(
222                            "Failed to retrieve token id mapping: {e}, type name: {name}"
223                        ))
224                    })
225            })
226            .collect()
227    }
228
229    pub async fn get_notional_values(&self) -> BridgeResult<HashMap<u8, u64>> {
230        let bridge_summary = self.get_bridge_summary().await?;
231        bridge_summary
232            .treasury
233            .id_token_type_map
234            .iter()
235            .map(|(id, type_name)| {
236                bridge_summary
237                    .treasury
238                    .supported_tokens
239                    .iter()
240                    .find_map(|(tn, metadata)| {
241                        if type_name == tn {
242                            Some((*id, metadata.notional_value))
243                        } else {
244                            None
245                        }
246                    })
247                    .ok_or(BridgeError::InternalError(
248                        "Error encountered when retrieving token notional values.".into(),
249                    ))
250            })
251            .collect()
252    }
253
254    pub async fn get_bridge_committee(&self) -> BridgeResult<BridgeCommittee> {
255        let bridge_summary = self.inner.get_bridge_summary().await?;
256        let move_type_bridge_committee = bridge_summary.committee;
257
258        let mut authorities = vec![];
259        // TODO: move this to MoveTypeBridgeCommittee
260        for (_, member) in move_type_bridge_committee.members {
261            let MoveTypeCommitteeMember {
262                sui_address,
263                bridge_pubkey_bytes,
264                voting_power,
265                http_rest_url,
266                blocklisted,
267            } = member;
268            let pubkey = BridgeAuthorityPublicKey::from_bytes(&bridge_pubkey_bytes)?;
269            let base_url = from_utf8(&http_rest_url).unwrap_or_else(|_e| {
270                warn!(
271                    "Bridge authority address: {}, pubkey: {:?} has invalid http url: {:?}",
272                    sui_address, bridge_pubkey_bytes, http_rest_url
273                );
274                ""
275            });
276            authorities.push(BridgeAuthority {
277                sui_address,
278                pubkey,
279                voting_power,
280                base_url: base_url.into(),
281                is_blocklisted: blocklisted,
282            });
283        }
284        BridgeCommittee::new(authorities)
285    }
286
287    pub async fn get_chain_identifier(&self) -> BridgeResult<String> {
288        self.inner.get_chain_identifier().await
289    }
290
291    pub async fn get_reference_gas_price_until_success(&self) -> u64 {
292        loop {
293            let Ok(Ok(rgp)) = retry_with_max_elapsed_time!(
294                self.inner.get_reference_gas_price(),
295                Duration::from_secs(30)
296            ) else {
297                self.bridge_metrics
298                    .sui_rpc_errors
299                    .with_label_values(&["get_reference_gas_price"])
300                    .inc();
301                error!("Failed to get reference gas price");
302                continue;
303            };
304            return rgp;
305        }
306    }
307
308    pub async fn get_latest_checkpoint_sequence_number(&self) -> BridgeResult<u64> {
309        self.inner.get_latest_checkpoint_sequence_number().await
310    }
311
312    pub async fn execute_transaction_block_with_effects(
313        &self,
314        tx: sui_types::transaction::Transaction,
315    ) -> BridgeResult<ExecuteTransactionResult> {
316        self.inner.execute_transaction_block_with_effects(tx).await
317    }
318
319    // TODO: this function is very slow (seconds) in tests, we need to optimize it
320    pub async fn get_token_transfer_action_onchain_status_until_success(
321        &self,
322        source_chain_id: u8,
323        seq_number: u64,
324    ) -> BridgeActionStatus {
325        loop {
326            let bridge_object_arg = self.get_mutable_bridge_object_arg_must_succeed().await;
327            let Ok(Ok(status)) = retry_with_max_elapsed_time!(
328                self.inner.get_token_transfer_action_onchain_status(
329                    bridge_object_arg,
330                    source_chain_id,
331                    seq_number
332                ),
333                Duration::from_secs(30)
334            ) else {
335                self.bridge_metrics
336                    .sui_rpc_errors
337                    .with_label_values(&["get_token_transfer_action_onchain_status"])
338                    .inc();
339                error!(
340                    source_chain_id,
341                    seq_number, "Failed to get token transfer action onchain status"
342                );
343                continue;
344            };
345            return status;
346        }
347    }
348
349    pub async fn get_token_transfer_action_onchain_signatures_until_success(
350        &self,
351        source_chain_id: u8,
352        seq_number: u64,
353    ) -> Option<Vec<Vec<u8>>> {
354        loop {
355            let bridge_object_arg = self.get_mutable_bridge_object_arg_must_succeed().await;
356            let Ok(Ok(sigs)) = retry_with_max_elapsed_time!(
357                self.inner.get_token_transfer_action_onchain_signatures(
358                    bridge_object_arg,
359                    source_chain_id,
360                    seq_number
361                ),
362                Duration::from_secs(30)
363            ) else {
364                self.bridge_metrics
365                    .sui_rpc_errors
366                    .with_label_values(&["get_token_transfer_action_onchain_signatures"])
367                    .inc();
368                error!(
369                    source_chain_id,
370                    seq_number, "Failed to get token transfer action onchain signatures"
371                );
372                continue;
373            };
374            return sigs;
375        }
376    }
377
378    pub async fn get_parsed_token_transfer_message(
379        &self,
380        source_chain_id: u8,
381        seq_number: u64,
382    ) -> BridgeResult<Option<ParsedTokenTransferMessage>> {
383        let bridge_object_arg = self.get_mutable_bridge_object_arg_must_succeed().await;
384        let message = self
385            .inner
386            .get_parsed_token_transfer_message(bridge_object_arg, source_chain_id, seq_number)
387            .await?;
388        Ok(match message {
389            Some(payload) => Some(ParsedTokenTransferMessage::try_from(payload)?),
390            None => None,
391        })
392    }
393
394    pub async fn get_bridge_record(
395        &self,
396        source_chain_id: u8,
397        seq_number: u64,
398    ) -> Result<Option<MoveTypeBridgeRecord>, BridgeError> {
399        self.inner
400            .get_bridge_record(source_chain_id, seq_number)
401            .await
402    }
403
404    pub async fn get_gas_data_panic_if_not_gas(
405        &self,
406        gas_object_id: ObjectID,
407    ) -> (GasCoin, ObjectRef, Owner) {
408        self.inner
409            .get_gas_data_panic_if_not_gas(gas_object_id)
410            .await
411    }
412
413    pub async fn get_bridge_records_in_range(
414        &self,
415        source_chain_id: u8,
416        start_seq_num: u64,
417        end_seq_num: u64,
418    ) -> Result<Vec<(u64, MoveTypeBridgeRecord)>, BridgeError> {
419        self.inner
420            .get_bridge_records_in_range(source_chain_id, start_seq_num, end_seq_num)
421            .await
422    }
423
424    pub async fn get_token_transfer_next_seq_number(
425        &self,
426        source_chain_id: u8,
427    ) -> Result<u64, BridgeError> {
428        self.inner
429            .get_token_transfer_next_seq_number(source_chain_id)
430            .await
431    }
432
433    /// Temporary measure to get corresponding sequence number cursor from a Bridge Module EventID
434    pub async fn get_sequence_number_from_event_id(
435        &self,
436        event_id: EventID,
437    ) -> BridgeResult<Option<u64>> {
438        let events = self
439            .inner
440            .get_events_by_tx_digest(event_id.tx_digest)
441            .await?;
442
443        let event = events
444            .events
445            .get(event_id.event_seq as usize)
446            .ok_or(BridgeError::NoBridgeEventsInTxPosition)?;
447
448        if event.type_.address.as_ref() != BRIDGE_PACKAGE_ID.as_ref() {
449            return Ok(None);
450        }
451
452        let bridge_event = match SuiBridgeEvent::try_from_sui_event(event)? {
453            Some(e) => e,
454            None => return Ok(None),
455        };
456
457        match bridge_event {
458            SuiBridgeEvent::SuiToEthTokenBridgeV1(event) => Ok(Some(event.nonce)),
459            _ => Ok(None),
460        }
461    }
462}
463
464/// Use a trait to abstract over the SuiSDKClient and SuiMockClient for testing.
465#[async_trait]
466pub trait SuiClientInner: Send + Sync {
467    async fn query_events(
468        &self,
469        query: EventFilter,
470        cursor: Option<EventID>,
471    ) -> Result<EventPage, BridgeError>;
472
473    async fn get_events_by_tx_digest(
474        &self,
475        tx_digest: TransactionDigest,
476    ) -> Result<SuiEvents, BridgeError>;
477
478    async fn get_chain_identifier(&self) -> Result<String, BridgeError>;
479
480    async fn get_reference_gas_price(&self) -> Result<u64, BridgeError>;
481
482    async fn get_latest_checkpoint_sequence_number(&self) -> Result<u64, BridgeError>;
483
484    async fn get_mutable_bridge_object_arg(&self) -> Result<ObjectArg, BridgeError>;
485
486    async fn get_bridge_summary(&self) -> Result<BridgeSummary, BridgeError>;
487
488    async fn execute_transaction_block_with_effects(
489        &self,
490        tx: Transaction,
491    ) -> Result<ExecuteTransactionResult, BridgeError>;
492
493    async fn get_token_transfer_action_onchain_status(
494        &self,
495        bridge_object_arg: ObjectArg,
496        source_chain_id: u8,
497        seq_number: u64,
498    ) -> Result<BridgeActionStatus, BridgeError>;
499
500    async fn get_token_transfer_action_onchain_signatures(
501        &self,
502        bridge_object_arg: ObjectArg,
503        source_chain_id: u8,
504        seq_number: u64,
505    ) -> Result<Option<Vec<Vec<u8>>>, BridgeError>;
506
507    async fn get_parsed_token_transfer_message(
508        &self,
509        bridge_object_arg: ObjectArg,
510        source_chain_id: u8,
511        seq_number: u64,
512    ) -> Result<Option<MoveTypeParsedTokenTransferMessage>, BridgeError>;
513
514    async fn get_bridge_record(
515        &self,
516        source_chain_id: u8,
517        seq_number: u64,
518    ) -> Result<Option<MoveTypeBridgeRecord>, BridgeError>;
519
520    async fn get_gas_data_panic_if_not_gas(
521        &self,
522        gas_object_id: ObjectID,
523    ) -> (GasCoin, ObjectRef, Owner);
524
525    async fn get_bridge_records_in_range(
526        &self,
527        source_chain_id: u8,
528        start_seq_num: u64,
529        end_seq_num: u64,
530    ) -> Result<Vec<(u64, MoveTypeBridgeRecord)>, BridgeError>;
531
532    async fn get_token_transfer_next_seq_number(
533        &self,
534        source_chain_id: u8,
535    ) -> Result<u64, BridgeError>;
536}
537
538#[async_trait]
539impl SuiClientInner for SuiSdkClient {
540    async fn query_events(
541        &self,
542        query: EventFilter,
543        cursor: Option<EventID>,
544    ) -> Result<EventPage, BridgeError> {
545        self.event_api()
546            .query_events(query, cursor, None, false)
547            .await
548            .map_err(Into::into)
549    }
550
551    async fn get_events_by_tx_digest(
552        &self,
553        _tx_digest: TransactionDigest,
554    ) -> Result<SuiEvents, BridgeError> {
555        unimplemented!("use gRPC implementation")
556    }
557
558    async fn get_chain_identifier(&self) -> Result<String, BridgeError> {
559        unimplemented!("use gRPC implementation")
560    }
561
562    async fn get_reference_gas_price(&self) -> Result<u64, BridgeError> {
563        unimplemented!("use gRPC implementation")
564    }
565
566    async fn get_latest_checkpoint_sequence_number(&self) -> Result<u64, BridgeError> {
567        unimplemented!("use gRPC implementation")
568    }
569
570    async fn get_mutable_bridge_object_arg(&self) -> Result<ObjectArg, BridgeError> {
571        unimplemented!("use gRPC implementation")
572    }
573
574    async fn get_bridge_summary(&self) -> Result<BridgeSummary, BridgeError> {
575        unimplemented!("use gRPC implementation")
576    }
577
578    async fn get_token_transfer_action_onchain_status(
579        &self,
580        _bridge_object_arg: ObjectArg,
581        _source_chain_id: u8,
582        _seq_number: u64,
583    ) -> Result<BridgeActionStatus, BridgeError> {
584        unimplemented!("use gRPC implementation")
585    }
586
587    async fn get_token_transfer_action_onchain_signatures(
588        &self,
589        _bridge_object_arg: ObjectArg,
590        _source_chain_id: u8,
591        _seq_number: u64,
592    ) -> Result<Option<Vec<Vec<u8>>>, BridgeError> {
593        unimplemented!("use gRPC implementation")
594    }
595
596    async fn execute_transaction_block_with_effects(
597        &self,
598        tx: Transaction,
599    ) -> Result<ExecuteTransactionResult, BridgeError> {
600        use sui_json_rpc_types::SuiTransactionBlockEffectsAPI;
601        match self.quorum_driver_api().execute_transaction_block(
602            tx,
603            SuiTransactionBlockResponseOptions::new().with_effects().with_events(),
604            Some(sui_types::transaction_driver_types::ExecuteTransactionRequestType::WaitForEffectsCert),
605        ).await {
606            Ok(response) => {
607                let effects = response.effects.expect("We requested effects but got None.");
608                let events = response.events.expect("We requested events but got None.");
609                Ok(ExecuteTransactionResult {
610                    status: effects.status().clone(),
611                    events: events.data,
612                })
613            }
614            Err(e) => Err(BridgeError::SuiTxFailureGeneric(e.to_string())),
615        }
616    }
617
618    async fn get_parsed_token_transfer_message(
619        &self,
620        _bridge_object_arg: ObjectArg,
621        _source_chain_id: u8,
622        _seq_number: u64,
623    ) -> Result<Option<MoveTypeParsedTokenTransferMessage>, BridgeError> {
624        unimplemented!("use gRPC implementation")
625    }
626
627    async fn get_bridge_record(
628        &self,
629        _source_chain_id: u8,
630        _seq_number: u64,
631    ) -> Result<Option<MoveTypeBridgeRecord>, BridgeError> {
632        unimplemented!("use gRPC implementation")
633    }
634
635    async fn get_gas_data_panic_if_not_gas(
636        &self,
637        gas_object_id: ObjectID,
638    ) -> (GasCoin, ObjectRef, Owner) {
639        loop {
640            match self
641                .read_api()
642                .get_object_with_options(
643                    gas_object_id,
644                    SuiObjectDataOptions::default().with_owner().with_content(),
645                )
646                .await
647                .map(|resp| resp.data)
648            {
649                Ok(Some(gas_obj)) => {
650                    let owner = gas_obj.owner.clone().expect("Owner is requested");
651                    let gas_coin = GasCoin::try_from(&gas_obj)
652                        .unwrap_or_else(|err| panic!("{} is not a gas coin: {err}", gas_object_id));
653                    return (gas_coin, gas_obj.object_ref(), owner);
654                }
655                other => {
656                    warn!("Can't get gas object: {:?}: {:?}", gas_object_id, other);
657                    tokio::time::sleep(Duration::from_secs(5)).await;
658                }
659            }
660        }
661    }
662
663    async fn get_bridge_records_in_range(
664        &self,
665        _source_chain_id: u8,
666        _start_seq_num: u64,
667        _end_seq_num: u64,
668    ) -> Result<Vec<(u64, MoveTypeBridgeRecord)>, BridgeError> {
669        unimplemented!("use gRPC implementation")
670    }
671
672    async fn get_token_transfer_next_seq_number(
673        &self,
674        _source_chain_id: u8,
675    ) -> Result<u64, BridgeError> {
676        unimplemented!("use gRPC implementation")
677    }
678}
679
680#[async_trait]
681impl SuiClientInner for sui_rpc::Client {
682    async fn query_events(
683        &self,
684        _query: EventFilter,
685        _cursor: Option<EventID>,
686    ) -> Result<EventPage, BridgeError> {
687        //TODO we'll need to reimplement the sui_syncer to iterate though records instead of
688        //querying events using this api
689        unimplemented!("query_events not supported in gRPC");
690    }
691
692    async fn get_events_by_tx_digest(
693        &self,
694        tx_digest: TransactionDigest,
695    ) -> Result<SuiEvents, BridgeError> {
696        let mut client = self.clone();
697        let resp = client
698            .ledger_client()
699            .get_transaction(
700                GetTransactionRequest::new(&(tx_digest.into())).with_read_mask(
701                    FieldMask::from_paths([
702                        ExecutedTransaction::path_builder().digest(),
703                        ExecutedTransaction::path_builder().events().finish(),
704                        ExecutedTransaction::path_builder().checkpoint(),
705                        ExecutedTransaction::path_builder().timestamp(),
706                    ]),
707                ),
708            )
709            .await?
710            .into_inner();
711        let resp = resp.transaction();
712
713        Ok(SuiEvents {
714            transaction_digest: tx_digest,
715            checkpoint: resp.checkpoint_opt(),
716            timestamp_ms: resp
717                .timestamp_opt()
718                .map(|timestamp| sui_rpc::proto::proto_to_timestamp_ms(*timestamp))
719                .transpose()?,
720            events: resp
721                .events()
722                .events()
723                .iter()
724                .enumerate()
725                .map(|(idx, event)| {
726                    Ok(SuiEvent {
727                        id: EventID {
728                            tx_digest,
729                            event_seq: idx as u64,
730                        },
731                        package_id: event.package_id().parse()?,
732                        transaction_module: Identifier::new(event.module())?,
733                        sender: event.sender().parse()?,
734                        type_: event.event_type().parse()?,
735                        parsed_json: Default::default(),
736                        bcs: BcsEvent::Base64 {
737                            bcs: event.contents().value().into(),
738                        },
739                        timestamp_ms: None,
740                    })
741                })
742                .collect::<Result<_, BridgeError>>()?,
743        })
744    }
745
746    async fn get_chain_identifier(&self) -> Result<String, BridgeError> {
747        let chain_id = self
748            .clone()
749            .ledger_client()
750            .get_service_info(GetServiceInfoRequest::default())
751            .await?
752            .into_inner()
753            .chain_id()
754            .parse::<sui_types::digests::CheckpointDigest>()?;
755
756        Ok(sui_types::digests::ChainIdentifier::from(chain_id).to_string())
757    }
758
759    async fn get_reference_gas_price(&self) -> Result<u64, BridgeError> {
760        let mut client = self.clone();
761        sui_rpc::Client::get_reference_gas_price(&mut client)
762            .await
763            .map_err(Into::into)
764    }
765
766    async fn get_latest_checkpoint_sequence_number(&self) -> Result<u64, BridgeError> {
767        let mut client = self.clone();
768        let resp =
769            client
770                .ledger_client()
771                .get_checkpoint(GetCheckpointRequest::latest().with_read_mask(
772                    FieldMask::from_paths([Checkpoint::path_builder().sequence_number()]),
773                ))
774                .await?
775                .into_inner();
776        Ok(resp.checkpoint().sequence_number())
777    }
778
779    async fn get_mutable_bridge_object_arg(&self) -> Result<ObjectArg, BridgeError> {
780        let owner = self
781            .clone()
782            .ledger_client()
783            .get_object(
784                GetObjectRequest::new(&(SUI_BRIDGE_OBJECT_ID.into())).with_read_mask(
785                    FieldMask::from_paths([Object::path_builder().owner().finish()]),
786                ),
787            )
788            .await?
789            .into_inner()
790            .object()
791            .owner()
792            .to_owned();
793        Ok(ObjectArg::SharedObject {
794            id: SUI_BRIDGE_OBJECT_ID,
795            initial_shared_version: SequenceNumber::from_u64(owner.version()),
796            mutability: SharedObjectMutability::Mutable,
797        })
798    }
799
800    async fn get_bridge_summary(&self) -> Result<BridgeSummary, BridgeError> {
801        static BRIDGE_VERSION_ID: tokio::sync::OnceCell<Address> =
802            tokio::sync::OnceCell::const_new();
803
804        let bridge_version_id = BRIDGE_VERSION_ID
805            .get_or_try_init::<BridgeError, _, _>(|| async {
806                let bridge_wrapper_bcs = self
807                    .clone()
808                    .ledger_client()
809                    .get_object(
810                        GetObjectRequest::new(&(SUI_BRIDGE_OBJECT_ID.into())).with_read_mask(
811                            FieldMask::from_paths([Object::path_builder().contents().finish()]),
812                        ),
813                    )
814                    .await?
815                    .into_inner()
816                    .object()
817                    .contents()
818                    .to_owned();
819
820                let bridge_wrapper: BridgeWrapper = bcs::from_bytes(bridge_wrapper_bcs.value())?;
821
822                Ok(bridge_wrapper.version.id.id.bytes.into())
823            })
824            .await?;
825
826        let bridge_inner_id = bridge_version_id
827            .derive_dynamic_child_id(&sui_sdk_types::TypeTag::U64, &bcs::to_bytes(&1u64).unwrap());
828
829        let field_bcs = self
830            .clone()
831            .ledger_client()
832            .get_object(GetObjectRequest::new(&bridge_inner_id).with_read_mask(
833                FieldMask::from_paths([Object::path_builder().contents().finish()]),
834            ))
835            .await?
836            .into_inner()
837            .object()
838            .contents()
839            .to_owned();
840
841        let field: sui_types::dynamic_field::Field<u64, sui_types::bridge::BridgeInnerV1> =
842            bcs::from_bytes(field_bcs.value())?;
843        let summary = field.value.try_into_bridge_summary()?;
844        Ok(summary)
845    }
846
847    async fn get_token_transfer_action_onchain_status(
848        &self,
849        _bridge_object_arg: ObjectArg,
850        source_chain_id: u8,
851        seq_number: u64,
852    ) -> Result<BridgeActionStatus, BridgeError> {
853        let record = self.get_bridge_record(source_chain_id, seq_number).await?;
854        let Some(record) = record else {
855            return Ok(BridgeActionStatus::NotFound);
856        };
857
858        if record.claimed {
859            Ok(BridgeActionStatus::Claimed)
860        } else if record.verified_signatures.is_some() {
861            Ok(BridgeActionStatus::Approved)
862        } else {
863            Ok(BridgeActionStatus::Pending)
864        }
865    }
866
867    async fn get_token_transfer_action_onchain_signatures(
868        &self,
869        _bridge_object_arg: ObjectArg,
870        source_chain_id: u8,
871        seq_number: u64,
872    ) -> Result<Option<Vec<Vec<u8>>>, BridgeError> {
873        let record = self.get_bridge_record(source_chain_id, seq_number).await?;
874        Ok(record.and_then(|record| record.verified_signatures))
875    }
876
877    async fn execute_transaction_block_with_effects(
878        &self,
879        tx: Transaction,
880    ) -> Result<ExecuteTransactionResult, BridgeError> {
881        use move_core_types::language_storage::StructTag;
882        use sui_rpc::proto::sui::rpc::v2::ExecutedTransaction as ProtoExecutedTransaction;
883        use sui_sdk_types::SignedTransaction;
884
885        let signed_tx: SignedTransaction = tx.try_into().map_err(|e| {
886            BridgeError::SuiTxFailureGeneric(format!("Failed to convert transaction: {:?}", e))
887        })?;
888
889        let proto_tx: ProtoTransaction = signed_tx.transaction.into();
890        let proto_sigs: Vec<ProtoUserSignature> =
891            signed_tx.signatures.into_iter().map(Into::into).collect();
892
893        let request = ExecuteTransactionRequest::default()
894            .with_transaction(proto_tx)
895            .with_signatures(proto_sigs)
896            .with_read_mask(FieldMask::from_paths([
897                ProtoExecutedTransaction::path_builder()
898                    .effects()
899                    .status()
900                    .finish(),
901                ProtoExecutedTransaction::path_builder()
902                    .events()
903                    .events()
904                    .finish(),
905            ]));
906
907        let response = self
908            .clone()
909            .execution_client()
910            .execute_transaction(request)
911            .await
912            .map_err(|e| BridgeError::SuiTxFailureGeneric(format!("gRPC execute failed: {:?}", e)))?
913            .into_inner();
914
915        let executed_tx = response.transaction();
916
917        let effects = executed_tx.effects();
918        let status = effects.status();
919
920        let sui_status = if status.success() {
921            SuiExecutionStatus::Success
922        } else {
923            let error = status.error();
924            let description = error.description().to_string();
925
926            let failure_msg = if !description.is_empty() {
927                description
928            } else {
929                format!("{:?}", error.kind())
930            };
931
932            SuiExecutionStatus::Failure { error: failure_msg }
933        };
934
935        let sui_events: Vec<SuiEvent> = executed_tx
936            .events()
937            .events()
938            .iter()
939            .filter_map(|event| {
940                let package_id: ObjectID = event.package_id().parse().ok()?;
941                let module = event.module().to_string();
942                let sender: sui_types::base_types::SuiAddress = event.sender().parse().ok()?;
943
944                let event_type_tag: sui_types::TypeTag =
945                    parse_sui_type_tag(event.event_type()).ok()?;
946                let struct_tag: StructTag = match event_type_tag {
947                    sui_types::TypeTag::Struct(s) => *s,
948                    _ => return None,
949                };
950                let contents = event.contents();
951                let bcs_bytes = contents.value().to_vec();
952
953                Some(SuiEvent {
954                    id: EventID {
955                        tx_digest: TransactionDigest::default(),
956                        event_seq: 0,
957                    },
958                    package_id,
959                    transaction_module: Identifier::new(module).ok()?,
960                    sender,
961                    type_: struct_tag,
962                    parsed_json: serde_json::Value::Null,
963                    bcs: BcsEvent::new(bcs_bytes),
964                    timestamp_ms: None,
965                })
966            })
967            .collect();
968
969        Ok(ExecuteTransactionResult {
970            status: sui_status,
971            events: sui_events,
972        })
973    }
974
975    async fn get_parsed_token_transfer_message(
976        &self,
977        _bridge_object_arg: ObjectArg,
978        source_chain_id: u8,
979        seq_number: u64,
980    ) -> Result<Option<MoveTypeParsedTokenTransferMessage>, BridgeError> {
981        let record = self.get_bridge_record(source_chain_id, seq_number).await?;
982
983        let Some(record) = record else {
984            return Ok(None);
985        };
986        let MoveTypeBridgeMessage {
987            message_type: _,
988            message_version,
989            seq_num,
990            source_chain,
991            payload,
992        } = record.message;
993
994        // Parse payload based on message version.
995        let parsed_payload: MoveTypeTokenTransferPayload = if message_version == 2 {
996            let mut v2: MoveTypeTokenTransferPayloadV2 = bcs::from_bytes(&payload)?;
997            v2.amount = u64::from_be_bytes(v2.amount.to_le_bytes());
998            v2.into()
999        } else {
1000            let mut v1: MoveTypeTokenTransferPayload = bcs::from_bytes(&payload)?;
1001            v1.amount = u64::from_be_bytes(v1.amount.to_le_bytes());
1002            v1
1003        };
1004
1005        Ok(Some(MoveTypeParsedTokenTransferMessage {
1006            message_version,
1007            seq_num,
1008            source_chain,
1009            payload,
1010            parsed_payload,
1011        }))
1012    }
1013
1014    async fn get_bridge_record(
1015        &self,
1016        source_chain_id: u8,
1017        seq_number: u64,
1018    ) -> Result<Option<MoveTypeBridgeRecord>, BridgeError> {
1019        static BRIDGE_RECORDS_ID: tokio::sync::OnceCell<Address> =
1020            tokio::sync::OnceCell::const_new();
1021
1022        let records_id = BRIDGE_RECORDS_ID
1023            .get_or_try_init(|| async {
1024                self.get_bridge_summary()
1025                    .await
1026                    .map(|summary| summary.bridge_records_id.into())
1027            })
1028            .await?;
1029
1030        let record_id = {
1031            let key = MoveTypeBridgeMessageKey {
1032                source_chain: source_chain_id,
1033                message_type: crate::types::BridgeActionType::TokenTransfer as u8,
1034                bridge_seq_num: seq_number,
1035            };
1036            let key_bytes = bcs::to_bytes(&key)?;
1037            let key_type = sui_sdk_types::StructTag::new(
1038                Address::from(BRIDGE_PACKAGE_ID),
1039                sui_sdk_types::Identifier::from_static("message"),
1040                sui_sdk_types::Identifier::from_static("BridgeMessageKey"),
1041                vec![],
1042            );
1043
1044            records_id.derive_dynamic_child_id(&(key_type.into()), &key_bytes)
1045        };
1046
1047        let response =
1048            match self
1049                .clone()
1050                .ledger_client()
1051                .get_object(GetObjectRequest::new(&record_id).with_read_mask(
1052                    FieldMask::from_paths([Object::path_builder().contents().finish()]),
1053                ))
1054                .await
1055            {
1056                Ok(response) => response,
1057                Err(status) => {
1058                    if status.code() == tonic::Code::NotFound {
1059                        return Ok(None);
1060                    } else {
1061                        return Err(status.into());
1062                    }
1063                }
1064            };
1065
1066        let field_bcs = response.into_inner().object().contents().to_owned();
1067
1068        let field: sui_types::dynamic_field::Field<
1069            MoveTypeBridgeMessageKey,
1070            LinkedTableNode<MoveTypeBridgeMessageKey, MoveTypeBridgeRecord>,
1071        > = bcs::from_bytes(field_bcs.value())?;
1072
1073        Ok(Some(field.value.value))
1074    }
1075
1076    async fn get_gas_data_panic_if_not_gas(
1077        &self,
1078        gas_object_id: ObjectID,
1079    ) -> (GasCoin, ObjectRef, Owner) {
1080        loop {
1081            let result = async {
1082                let resp = self
1083                    .clone()
1084                    .ledger_client()
1085                    .get_object(
1086                        GetObjectRequest::new(&(gas_object_id.into())).with_read_mask(
1087                            FieldMask::from_paths([Object::path_builder().bcs().finish()]),
1088                        ),
1089                    )
1090                    .await?
1091                    .into_inner();
1092
1093                let obj = resp.object();
1094                let object: sui_types::object::Object = obj.bcs().deserialize().map_err(|e| {
1095                    BridgeError::Generic(format!("Failed to deserialize object from BCS: {e}"))
1096                })?;
1097
1098                let object_ref = object.compute_object_reference();
1099                let owner = object.owner().clone();
1100                let gas_coin = GasCoin::try_from(&object).map_err(|e| {
1101                    BridgeError::Generic(format!("Failed to convert object to gas coin: {e}"))
1102                })?;
1103
1104                Ok::<_, BridgeError>((gas_coin, object_ref, owner))
1105            }
1106            .await;
1107
1108            match result {
1109                Ok(data) => return data,
1110                Err(e) => {
1111                    warn!("Can't get gas object: {:?}: {:?}", gas_object_id, e);
1112                    tokio::time::sleep(Duration::from_secs(5)).await;
1113                }
1114            }
1115        }
1116    }
1117
1118    async fn get_bridge_records_in_range(
1119        &self,
1120        source_chain_id: u8,
1121        start_seq_num: u64,
1122        end_seq_num: u64,
1123    ) -> Result<Vec<(u64, MoveTypeBridgeRecord)>, BridgeError> {
1124        let mut records = Vec::new();
1125        for seq_num in start_seq_num..=end_seq_num {
1126            if let Some(record) = self.get_bridge_record(source_chain_id, seq_num).await? {
1127                records.push((seq_num, record));
1128            }
1129        }
1130        Ok(records)
1131    }
1132
1133    async fn get_token_transfer_next_seq_number(
1134        &self,
1135        source_chain_id: u8,
1136    ) -> Result<u64, BridgeError> {
1137        let summary = self.get_bridge_summary().await?;
1138        let seq_num = summary
1139            .sequence_nums
1140            .iter()
1141            .find(|(chain_id, _)| *chain_id == source_chain_id)
1142            .map(|(_, seq)| *seq)
1143            .unwrap_or(0);
1144        Ok(seq_num)
1145    }
1146}
1147
1148#[async_trait]
1149impl SuiClientInner for SuiClientInternal {
1150    async fn query_events(
1151        &self,
1152        query: EventFilter,
1153        cursor: Option<EventID>,
1154    ) -> Result<EventPage, BridgeError> {
1155        self.jsonrpc_client.query_events(query, cursor).await
1156    }
1157
1158    async fn get_events_by_tx_digest(
1159        &self,
1160        tx_digest: TransactionDigest,
1161    ) -> Result<SuiEvents, BridgeError> {
1162        self.grpc_client.get_events_by_tx_digest(tx_digest).await
1163    }
1164
1165    async fn get_chain_identifier(&self) -> Result<String, BridgeError> {
1166        self.grpc_client.get_chain_identifier().await
1167    }
1168
1169    async fn get_reference_gas_price(&self) -> Result<u64, BridgeError> {
1170        self.grpc_client.get_reference_gas_price().await
1171    }
1172
1173    async fn get_latest_checkpoint_sequence_number(&self) -> Result<u64, BridgeError> {
1174        self.grpc_client
1175            .get_latest_checkpoint_sequence_number()
1176            .await
1177    }
1178
1179    async fn get_mutable_bridge_object_arg(&self) -> Result<ObjectArg, BridgeError> {
1180        self.grpc_client.get_mutable_bridge_object_arg().await
1181    }
1182
1183    async fn get_bridge_summary(&self) -> Result<BridgeSummary, BridgeError> {
1184        self.grpc_client.get_bridge_summary().await
1185    }
1186
1187    async fn get_token_transfer_action_onchain_status(
1188        &self,
1189        bridge_object_arg: ObjectArg,
1190        source_chain_id: u8,
1191        seq_number: u64,
1192    ) -> Result<BridgeActionStatus, BridgeError> {
1193        self.grpc_client
1194            .get_token_transfer_action_onchain_status(
1195                bridge_object_arg,
1196                source_chain_id,
1197                seq_number,
1198            )
1199            .await
1200    }
1201
1202    async fn get_token_transfer_action_onchain_signatures(
1203        &self,
1204        bridge_object_arg: ObjectArg,
1205        source_chain_id: u8,
1206        seq_number: u64,
1207    ) -> Result<Option<Vec<Vec<u8>>>, BridgeError> {
1208        self.grpc_client
1209            .get_token_transfer_action_onchain_signatures(
1210                bridge_object_arg,
1211                source_chain_id,
1212                seq_number,
1213            )
1214            .await
1215    }
1216
1217    async fn execute_transaction_block_with_effects(
1218        &self,
1219        tx: Transaction,
1220    ) -> Result<ExecuteTransactionResult, BridgeError> {
1221        self.grpc_client
1222            .execute_transaction_block_with_effects(tx)
1223            .await
1224    }
1225
1226    async fn get_parsed_token_transfer_message(
1227        &self,
1228        bridge_object_arg: ObjectArg,
1229        source_chain_id: u8,
1230        seq_number: u64,
1231    ) -> Result<Option<MoveTypeParsedTokenTransferMessage>, BridgeError> {
1232        self.grpc_client
1233            .get_parsed_token_transfer_message(bridge_object_arg, source_chain_id, seq_number)
1234            .await
1235    }
1236
1237    async fn get_bridge_record(
1238        &self,
1239        source_chain_id: u8,
1240        seq_number: u64,
1241    ) -> Result<Option<MoveTypeBridgeRecord>, BridgeError> {
1242        self.grpc_client
1243            .get_bridge_record(source_chain_id, seq_number)
1244            .await
1245    }
1246
1247    async fn get_gas_data_panic_if_not_gas(
1248        &self,
1249        gas_object_id: ObjectID,
1250    ) -> (GasCoin, ObjectRef, Owner) {
1251        self.jsonrpc_client
1252            .get_gas_data_panic_if_not_gas(gas_object_id)
1253            .await
1254    }
1255
1256    async fn get_bridge_records_in_range(
1257        &self,
1258        source_chain_id: u8,
1259        start_seq_num: u64,
1260        end_seq_num: u64,
1261    ) -> Result<Vec<(u64, MoveTypeBridgeRecord)>, BridgeError> {
1262        self.grpc_client
1263            .get_bridge_records_in_range(source_chain_id, start_seq_num, end_seq_num)
1264            .await
1265    }
1266
1267    async fn get_token_transfer_next_seq_number(
1268        &self,
1269        source_chain_id: u8,
1270    ) -> Result<u64, BridgeError> {
1271        self.grpc_client
1272            .get_token_transfer_next_seq_number(source_chain_id)
1273            .await
1274    }
1275}
1276
1277#[cfg(test)]
1278mod tests {
1279    use crate::crypto::BridgeAuthorityKeyPair;
1280    use crate::e2e_tests::test_utils::TestClusterWrapperBuilder;
1281    use crate::types::SuiToEthTokenTransfer;
1282    use crate::{
1283        events::{EmittedSuiToEthTokenBridgeV1, MoveTokenDepositedEvent},
1284        sui_mock_client::SuiMockClient,
1285        test_utils::{
1286            approve_action_with_validator_secrets, bridge_token, get_test_eth_to_sui_bridge_action,
1287            get_test_sui_to_eth_bridge_action,
1288        },
1289    };
1290    use alloy::primitives::Address as EthAddress;
1291    use move_core_types::account_address::AccountAddress;
1292    use serde::{Deserialize, Serialize};
1293    use std::str::FromStr;
1294    use sui_json_rpc_types::BcsEvent;
1295    use sui_types::base_types::SuiAddress;
1296    use sui_types::bridge::{BridgeChainId, TOKEN_ID_SUI, TOKEN_ID_USDC};
1297    use sui_types::crypto::get_key_pair;
1298
1299    use super::*;
1300    use crate::events::{SuiToEthTokenBridgeV1, init_all_struct_tags};
1301
1302    #[tokio::test]
1303    async fn get_bridge_action_by_tx_digest_and_event_idx_maybe() {
1304        // Note: for random events generated in this test, we only care about
1305        // tx_digest and event_seq, so it's ok that package and module does
1306        // not match the query parameters.
1307        telemetry_subscribers::init_for_testing();
1308        let mock_client = SuiMockClient::default();
1309        let sui_client = SuiClient::new_for_testing(mock_client.clone());
1310        let tx_digest = TransactionDigest::random();
1311
1312        // Ensure all struct tags are inited
1313        init_all_struct_tags();
1314
1315        let sanitized_event_1 = EmittedSuiToEthTokenBridgeV1 {
1316            nonce: 1,
1317            sui_chain_id: BridgeChainId::SuiTestnet,
1318            sui_address: SuiAddress::random_for_testing_only(),
1319            eth_chain_id: BridgeChainId::EthSepolia,
1320            eth_address: EthAddress::random(),
1321            token_id: TOKEN_ID_SUI,
1322            amount_sui_adjusted: 100,
1323        };
1324        let emitted_event_1 = MoveTokenDepositedEvent {
1325            seq_num: sanitized_event_1.nonce,
1326            source_chain: sanitized_event_1.sui_chain_id as u8,
1327            sender_address: sanitized_event_1.sui_address.to_vec(),
1328            target_chain: sanitized_event_1.eth_chain_id as u8,
1329            target_address: sanitized_event_1.eth_address.to_vec(),
1330            token_type: sanitized_event_1.token_id,
1331            amount_sui_adjusted: sanitized_event_1.amount_sui_adjusted,
1332        };
1333
1334        let mut sui_event_1 = SuiEvent::random_for_testing();
1335        sui_event_1.type_ = SuiToEthTokenBridgeV1.get().unwrap().clone();
1336        sui_event_1.bcs = BcsEvent::new(bcs::to_bytes(&emitted_event_1).unwrap());
1337
1338        #[derive(Serialize, Deserialize)]
1339        struct RandomStruct {}
1340
1341        let event_2: RandomStruct = RandomStruct {};
1342        // undeclared struct tag
1343        let mut sui_event_2 = SuiEvent::random_for_testing();
1344        sui_event_2.type_ = SuiToEthTokenBridgeV1.get().unwrap().clone();
1345        sui_event_2.type_.module = Identifier::from_str("unrecognized_module").unwrap();
1346        sui_event_2.bcs = BcsEvent::new(bcs::to_bytes(&event_2).unwrap());
1347
1348        // Event 3 is defined in non-bridge package
1349        let mut sui_event_3 = sui_event_1.clone();
1350        sui_event_3.type_.address = AccountAddress::random();
1351
1352        mock_client.add_events_by_tx_digest(
1353            tx_digest,
1354            vec![
1355                sui_event_1.clone(),
1356                sui_event_2.clone(),
1357                sui_event_1.clone(),
1358                sui_event_3.clone(),
1359            ],
1360        );
1361        let expected_action = BridgeAction::SuiToEthTokenTransfer(SuiToEthTokenTransfer {
1362            nonce: sanitized_event_1.nonce,
1363            sui_chain_id: sanitized_event_1.sui_chain_id,
1364            eth_chain_id: sanitized_event_1.eth_chain_id,
1365            sui_address: sanitized_event_1.sui_address,
1366            eth_address: sanitized_event_1.eth_address,
1367            token_id: sanitized_event_1.token_id,
1368            amount_adjusted: sanitized_event_1.amount_sui_adjusted,
1369        });
1370        assert_eq!(
1371            sui_client
1372                .get_bridge_action_by_tx_digest_and_event_idx_maybe(&tx_digest, 0)
1373                .await
1374                .unwrap(),
1375            expected_action,
1376        );
1377        assert_eq!(
1378            sui_client
1379                .get_bridge_action_by_tx_digest_and_event_idx_maybe(&tx_digest, 2)
1380                .await
1381                .unwrap(),
1382            expected_action,
1383        );
1384        assert!(matches!(
1385            sui_client
1386                .get_bridge_action_by_tx_digest_and_event_idx_maybe(&tx_digest, 1)
1387                .await
1388                .unwrap_err(),
1389            BridgeError::NoBridgeEventsInTxPosition
1390        ),);
1391        assert!(matches!(
1392            sui_client
1393                .get_bridge_action_by_tx_digest_and_event_idx_maybe(&tx_digest, 3)
1394                .await
1395                .unwrap_err(),
1396            BridgeError::BridgeEventInUnrecognizedSuiPackage
1397        ),);
1398        assert!(matches!(
1399            sui_client
1400                .get_bridge_action_by_tx_digest_and_event_idx_maybe(&tx_digest, 4)
1401                .await
1402                .unwrap_err(),
1403            BridgeError::NoBridgeEventsInTxPosition
1404        ),);
1405
1406        // if the StructTag matches with unparsable bcs, it returns an error
1407        sui_event_2.type_ = SuiToEthTokenBridgeV1.get().unwrap().clone();
1408        mock_client.add_events_by_tx_digest(tx_digest, vec![sui_event_2]);
1409        sui_client
1410            .get_bridge_action_by_tx_digest_and_event_idx_maybe(&tx_digest, 2)
1411            .await
1412            .unwrap_err();
1413    }
1414
1415    // Test get_action_onchain_status.
1416    // Use validator secrets to bridge USDC from Ethereum initially.
1417    // TODO: we need an e2e test for this with published solidity contract and committee with BridgeNodes
1418    #[tokio::test(flavor = "multi_thread", worker_threads = 8)]
1419    async fn test_get_action_onchain_status_for_sui_to_eth_transfer() {
1420        telemetry_subscribers::init_for_testing();
1421        let mut bridge_keys = vec![];
1422        for _ in 0..=3 {
1423            let (_, kp): (_, BridgeAuthorityKeyPair) = get_key_pair();
1424            bridge_keys.push(kp);
1425        }
1426        let mut test_cluster = TestClusterWrapperBuilder::new()
1427            .with_bridge_authority_keys(bridge_keys)
1428            .with_deploy_tokens(true)
1429            .build()
1430            .await;
1431
1432        let bridge_metrics = Arc::new(BridgeMetrics::new_for_testing());
1433        let sui_client =
1434            SuiClient::new(&test_cluster.inner.fullnode_handle.rpc_url, bridge_metrics)
1435                .await
1436                .unwrap();
1437        let bridge_authority_keys = test_cluster.authority_keys_clone();
1438
1439        // Wait until committee is set up
1440        test_cluster
1441            .trigger_reconfiguration_if_not_yet_and_assert_bridge_committee_initialized()
1442            .await;
1443        let context = &mut test_cluster.inner.wallet;
1444        let sender = context.active_address().unwrap();
1445        let usdc_amount = 5000000;
1446        let bridge_object_arg = sui_client
1447            .get_mutable_bridge_object_arg_must_succeed()
1448            .await;
1449        let id_token_map = sui_client.get_token_id_map().await.unwrap();
1450
1451        // 1. Create a Eth -> Sui Transfer (recipient is sender address), approve with validator secrets and assert its status to be Claimed
1452        let action = get_test_eth_to_sui_bridge_action(None, Some(usdc_amount), Some(sender), None);
1453        let usdc_object_ref = approve_action_with_validator_secrets(
1454            context,
1455            bridge_object_arg,
1456            action.clone(),
1457            &bridge_authority_keys,
1458            Some(sender),
1459            &id_token_map,
1460        )
1461        .await
1462        .unwrap();
1463
1464        let status = sui_client
1465            .inner
1466            .get_token_transfer_action_onchain_status(
1467                bridge_object_arg,
1468                action.chain_id() as u8,
1469                action.seq_number(),
1470            )
1471            .await
1472            .unwrap();
1473        assert_eq!(status, BridgeActionStatus::Claimed);
1474
1475        // 2. Create a Sui -> Eth Transfer, approve with validator secrets and assert its status to be Approved
1476        // We need to actually send tokens to bridge to initialize the record.
1477        let eth_recv_address = EthAddress::random();
1478        let bridge_event = bridge_token(
1479            context,
1480            eth_recv_address,
1481            usdc_object_ref,
1482            id_token_map.get(&TOKEN_ID_USDC).unwrap().clone(),
1483            bridge_object_arg,
1484        )
1485        .await;
1486        assert_eq!(bridge_event.nonce, 0);
1487        assert_eq!(bridge_event.sui_chain_id, BridgeChainId::SuiCustom);
1488        assert_eq!(bridge_event.eth_chain_id, BridgeChainId::EthCustom);
1489        assert_eq!(bridge_event.eth_address, eth_recv_address);
1490        assert_eq!(bridge_event.sui_address, sender);
1491        assert_eq!(bridge_event.token_id, TOKEN_ID_USDC);
1492        assert_eq!(bridge_event.amount_sui_adjusted, usdc_amount);
1493
1494        let action = get_test_sui_to_eth_bridge_action(
1495            None,
1496            None,
1497            Some(bridge_event.nonce),
1498            Some(bridge_event.amount_sui_adjusted),
1499            Some(bridge_event.sui_address),
1500            Some(bridge_event.eth_address),
1501            Some(TOKEN_ID_USDC),
1502        );
1503        let status = sui_client
1504            .inner
1505            .get_token_transfer_action_onchain_status(
1506                bridge_object_arg,
1507                action.chain_id() as u8,
1508                action.seq_number(),
1509            )
1510            .await
1511            .unwrap();
1512        // At this point, the record is created and the status is Pending
1513        assert_eq!(status, BridgeActionStatus::Pending);
1514
1515        // Approve it and assert its status to be Approved
1516        approve_action_with_validator_secrets(
1517            context,
1518            bridge_object_arg,
1519            action.clone(),
1520            &bridge_authority_keys,
1521            None,
1522            &id_token_map,
1523        )
1524        .await;
1525
1526        let status = sui_client
1527            .inner
1528            .get_token_transfer_action_onchain_status(
1529                bridge_object_arg,
1530                action.chain_id() as u8,
1531                action.seq_number(),
1532            )
1533            .await
1534            .unwrap();
1535        assert_eq!(status, BridgeActionStatus::Approved);
1536
1537        // 3. Create a random action and assert its status as NotFound
1538        let action =
1539            get_test_sui_to_eth_bridge_action(None, None, Some(100), None, None, None, None);
1540        let status = sui_client
1541            .inner
1542            .get_token_transfer_action_onchain_status(
1543                bridge_object_arg,
1544                action.chain_id() as u8,
1545                action.seq_number(),
1546            )
1547            .await
1548            .unwrap();
1549        assert_eq!(status, BridgeActionStatus::NotFound);
1550    }
1551}