sui_bridge/
sui_client.rs

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