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