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