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