1use 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 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 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 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 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 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 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#[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 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 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 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 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 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 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 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 #[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 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 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 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 assert_eq!(status, BridgeActionStatus::Pending);
1316
1317 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 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}