iota_bridge/
iota_client.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use core::panic;
6use std::{collections::HashMap, str::from_utf8, time::Duration};
7
8use anyhow::anyhow;
9use async_trait::async_trait;
10use fastcrypto::traits::ToFromBytes;
11use iota_json_rpc_api::BridgeReadApiClient;
12use iota_json_rpc_types::{
13    DevInspectResults, EventFilter, EventPage, IotaEvent, IotaObjectDataOptions,
14    IotaTransactionBlockResponse, IotaTransactionBlockResponseOptions, Page,
15};
16use iota_sdk::{IotaClient as IotaSdkClient, IotaClientBuilder};
17use iota_types::{
18    BRIDGE_PACKAGE_ID, IOTA_BRIDGE_OBJECT_ID, Identifier, TypeTag,
19    base_types::{IotaAddress, ObjectID, ObjectRef, SequenceNumber},
20    bridge::{
21        BridgeSummary, BridgeTreasurySummary, MoveTypeCommitteeMember,
22        MoveTypeParsedTokenTransferMessage,
23    },
24    digests::TransactionDigest,
25    event::EventID,
26    gas_coin::GasCoin,
27    object::Owner,
28    parse_iota_type_tag,
29    transaction::{
30        Argument, CallArg, Command, ObjectArg, ProgrammableMoveCall, ProgrammableTransaction,
31        Transaction, TransactionKind,
32    },
33};
34use serde::de::DeserializeOwned;
35use tokio::sync::OnceCell;
36use tracing::{error, warn};
37
38use crate::{
39    crypto::BridgeAuthorityPublicKey,
40    error::{BridgeError, BridgeResult},
41    events::IotaBridgeEvent,
42    retry_with_max_elapsed_time,
43    types::{
44        BridgeAction, BridgeActionStatus, BridgeAuthority, BridgeCommittee,
45        ParsedTokenTransferMessage,
46    },
47};
48
49pub struct IotaClient<P> {
50    inner: P,
51}
52
53pub type IotaBridgeClient = IotaClient<IotaSdkClient>;
54
55impl IotaBridgeClient {
56    pub async fn new(rpc_url: &str) -> anyhow::Result<Self> {
57        let inner = IotaClientBuilder::default()
58            .build(rpc_url)
59            .await
60            .map_err(|e| {
61                anyhow!("Can't establish connection with IOTA Rpc {rpc_url}. Error: {e}")
62            })?;
63        let self_ = Self { inner };
64        self_.describe().await?;
65        Ok(self_)
66    }
67
68    pub fn iota_client(&self) -> &IotaSdkClient {
69        &self.inner
70    }
71}
72
73impl<P> IotaClient<P>
74where
75    P: IotaClientInner,
76{
77    pub fn new_for_testing(inner: P) -> Self {
78        Self { inner }
79    }
80
81    // TODO assert chain identifier
82    async fn describe(&self) -> anyhow::Result<()> {
83        let chain_id = self.inner.get_chain_identifier().await?;
84        let block_number = self.inner.get_latest_checkpoint_sequence_number().await?;
85        tracing::info!(
86            "IotaClient is connected to chain {chain_id}, current block number: {block_number}"
87        );
88        Ok(())
89    }
90
91    /// Get the mutable bridge object arg on chain.
92    // We retry a few times in case of errors. If it fails eventually, we panic.
93    // In general it's safe to call in the beginning of the program.
94    // After the first call, the result is cached since the value should never
95    // change.
96    pub async fn get_mutable_bridge_object_arg_must_succeed(&self) -> ObjectArg {
97        static ARG: OnceCell<ObjectArg> = OnceCell::const_new();
98        *ARG.get_or_init(|| async move {
99            let Ok(Ok(bridge_object_arg)) = retry_with_max_elapsed_time!(
100                self.inner.get_mutable_bridge_object_arg(),
101                Duration::from_secs(30)
102            ) else {
103                panic!("Failed to get bridge object arg after retries");
104            };
105            bridge_object_arg
106        })
107        .await
108    }
109
110    /// Query emitted Events that are defined in the given Move Module.
111    pub async fn query_events_by_module(
112        &self,
113        package: ObjectID,
114        module: Identifier,
115        // cursor is exclusive
116        cursor: Option<EventID>,
117    ) -> BridgeResult<Page<IotaEvent, EventID>> {
118        let filter = EventFilter::MoveEventModule {
119            package,
120            module: module.clone(),
121        };
122        let events = self.inner.query_events(filter.clone(), cursor).await?;
123
124        // Safeguard check that all events are emitted from requested package and module
125        assert!(
126            events
127                .data
128                .iter()
129                .all(|event| event.type_.address.as_ref() == package.as_ref()
130                    && event.type_.module == module)
131        );
132        Ok(events)
133    }
134
135    /// Returns BridgeAction from an IOTA Transaction with transaction hash
136    /// and the event index. If event is declared in an unrecognized
137    /// package, return error.
138    pub async fn get_bridge_action_by_tx_digest_and_event_idx_maybe(
139        &self,
140        tx_digest: &TransactionDigest,
141        event_idx: u16,
142    ) -> BridgeResult<BridgeAction> {
143        let events = self.inner.get_events_by_tx_digest(*tx_digest).await?;
144        let event = events
145            .get(event_idx as usize)
146            .ok_or(BridgeError::NoBridgeEventsInTxPosition)?;
147        if event.type_.address.as_ref() != BRIDGE_PACKAGE_ID.as_ref() {
148            return Err(BridgeError::BridgeEventInUnrecognizedIotaPackage);
149        }
150        let bridge_event = IotaBridgeEvent::try_from_iota_event(event)?
151            .ok_or(BridgeError::NoBridgeEventsInTxPosition)?;
152
153        bridge_event
154            .try_into_bridge_action(*tx_digest, event_idx)
155            .ok_or(BridgeError::BridgeEventNotActionable)
156    }
157
158    pub async fn get_bridge_summary(&self) -> BridgeResult<BridgeSummary> {
159        self.inner
160            .get_bridge_summary()
161            .await
162            .map_err(|e| BridgeError::Internal(format!("Can't get bridge committee: {e}")))
163    }
164
165    pub async fn is_bridge_paused(&self) -> BridgeResult<bool> {
166        self.get_bridge_summary()
167            .await
168            .map(|summary| summary.is_frozen)
169    }
170
171    pub async fn get_treasury_summary(&self) -> BridgeResult<BridgeTreasurySummary> {
172        Ok(self.get_bridge_summary().await?.treasury)
173    }
174
175    pub async fn get_token_id_map(&self) -> BridgeResult<HashMap<u8, TypeTag>> {
176        self.get_bridge_summary()
177            .await?
178            .treasury
179            .id_token_type_map
180            .into_iter()
181            .map(|(id, name)| {
182                parse_iota_type_tag(&format!("0x{name}"))
183                    .map(|name| (id, name))
184                    .map_err(|e| {
185                        BridgeError::Internal(format!(
186                            "Failed to retrieve token id mapping: {e}, type name: {name}"
187                        ))
188                    })
189            })
190            .collect()
191    }
192
193    pub async fn get_notional_values(&self) -> BridgeResult<HashMap<u8, u64>> {
194        let bridge_summary = self.get_bridge_summary().await?;
195        bridge_summary
196            .treasury
197            .id_token_type_map
198            .iter()
199            .map(|(id, type_name)| {
200                bridge_summary
201                    .treasury
202                    .supported_tokens
203                    .iter()
204                    .find_map(|(tn, metadata)| {
205                        if type_name == tn {
206                            Some((*id, metadata.notional_value))
207                        } else {
208                            None
209                        }
210                    })
211                    .ok_or(BridgeError::Internal(
212                        "Error encountered when retrieving token notional values.".into(),
213                    ))
214            })
215            .collect()
216    }
217
218    pub async fn get_bridge_committee(&self) -> BridgeResult<BridgeCommittee> {
219        let bridge_summary = self
220            .inner
221            .get_bridge_summary()
222            .await
223            .map_err(|e| BridgeError::Internal(format!("Can't get bridge committee: {e}")))?;
224        let move_type_bridge_committee = bridge_summary.committee;
225
226        let mut authorities = vec![];
227        // TODO: move this to MoveTypeBridgeCommittee
228        for (_, member) in move_type_bridge_committee.members {
229            let MoveTypeCommitteeMember {
230                iota_address,
231                bridge_pubkey_bytes,
232                voting_power,
233                http_rest_url,
234                blocklisted,
235            } = member;
236            let pubkey = BridgeAuthorityPublicKey::from_bytes(&bridge_pubkey_bytes)?;
237            let base_url = from_utf8(&http_rest_url).unwrap_or_else(|_e| {
238                warn!(
239                    "Bridge authority address: {}, pubkey: {:?} has invalid http url: {:?}",
240                    iota_address, bridge_pubkey_bytes, http_rest_url
241                );
242                ""
243            });
244            authorities.push(BridgeAuthority {
245                pubkey,
246                voting_power,
247                base_url: base_url.into(),
248                is_blocklisted: blocklisted,
249            });
250        }
251        BridgeCommittee::new(authorities)
252    }
253
254    pub async fn get_chain_identifier(&self) -> BridgeResult<String> {
255        Ok(self.inner.get_chain_identifier().await?)
256    }
257
258    pub async fn get_reference_gas_price_until_success(&self) -> u64 {
259        loop {
260            let Ok(Ok(rgp)) = retry_with_max_elapsed_time!(
261                self.inner.get_reference_gas_price(),
262                Duration::from_secs(30)
263            ) else {
264                // TODO: add metrics and fire alert
265                error!("Failed to get reference gas price");
266                continue;
267            };
268            return rgp;
269        }
270    }
271
272    pub async fn execute_transaction_block_with_effects(
273        &self,
274        tx: iota_types::transaction::Transaction,
275    ) -> BridgeResult<IotaTransactionBlockResponse> {
276        self.inner.execute_transaction_block_with_effects(tx).await
277    }
278
279    // TODO: this function is very slow (seconds) in tests, we need to optimize it
280    pub async fn get_token_transfer_action_onchain_status_until_success(
281        &self,
282        source_chain_id: u8,
283        seq_number: u64,
284    ) -> BridgeActionStatus {
285        loop {
286            let bridge_object_arg = self.get_mutable_bridge_object_arg_must_succeed().await;
287            let Ok(Ok(status)) = retry_with_max_elapsed_time!(
288                self.inner.get_token_transfer_action_onchain_status(
289                    bridge_object_arg,
290                    source_chain_id,
291                    seq_number
292                ),
293                Duration::from_secs(30)
294            ) else {
295                // TODO: add metrics and fire alert
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                // TODO: add metrics and fire alert
322                error!(
323                    source_chain_id,
324                    seq_number, "Failed to get token transfer action onchain signatures"
325                );
326                continue;
327            };
328            return sigs;
329        }
330    }
331
332    pub async fn get_parsed_token_transfer_message(
333        &self,
334        source_chain_id: u8,
335        seq_number: u64,
336    ) -> BridgeResult<Option<ParsedTokenTransferMessage>> {
337        let bridge_object_arg = self.get_mutable_bridge_object_arg_must_succeed().await;
338        let message = self
339            .inner
340            .get_parsed_token_transfer_message(bridge_object_arg, source_chain_id, seq_number)
341            .await?;
342        Ok(match message {
343            Some(payload) => Some(ParsedTokenTransferMessage::try_from(payload)?),
344            None => None,
345        })
346    }
347
348    pub async fn get_gas_data_panic_if_not_gas(
349        &self,
350        gas_object_id: ObjectID,
351    ) -> (GasCoin, ObjectRef, Owner) {
352        self.inner
353            .get_gas_data_panic_if_not_gas(gas_object_id)
354            .await
355    }
356}
357
358/// Use a trait to abstract over the IotaSDKClient and IotaMockClient for
359/// testing.
360#[async_trait]
361pub trait IotaClientInner: Send + Sync {
362    type Error: Into<anyhow::Error> + Send + Sync + std::error::Error + 'static;
363    async fn query_events(
364        &self,
365        query: EventFilter,
366        cursor: Option<EventID>,
367    ) -> Result<EventPage, Self::Error>;
368
369    async fn get_events_by_tx_digest(
370        &self,
371        tx_digest: TransactionDigest,
372    ) -> Result<Vec<IotaEvent>, Self::Error>;
373
374    async fn get_chain_identifier(&self) -> Result<String, Self::Error>;
375
376    async fn get_reference_gas_price(&self) -> Result<u64, Self::Error>;
377
378    async fn get_latest_checkpoint_sequence_number(&self) -> Result<u64, Self::Error>;
379
380    async fn get_mutable_bridge_object_arg(&self) -> Result<ObjectArg, Self::Error>;
381
382    async fn get_bridge_summary(&self) -> Result<BridgeSummary, Self::Error>;
383
384    async fn execute_transaction_block_with_effects(
385        &self,
386        tx: Transaction,
387    ) -> Result<IotaTransactionBlockResponse, BridgeError>;
388
389    async fn get_token_transfer_action_onchain_status(
390        &self,
391        bridge_object_arg: ObjectArg,
392        source_chain_id: u8,
393        seq_number: u64,
394    ) -> Result<BridgeActionStatus, BridgeError>;
395
396    async fn get_token_transfer_action_onchain_signatures(
397        &self,
398        bridge_object_arg: ObjectArg,
399        source_chain_id: u8,
400        seq_number: u64,
401    ) -> Result<Option<Vec<Vec<u8>>>, BridgeError>;
402
403    async fn get_parsed_token_transfer_message(
404        &self,
405        bridge_object_arg: ObjectArg,
406        source_chain_id: u8,
407        seq_number: u64,
408    ) -> Result<Option<MoveTypeParsedTokenTransferMessage>, BridgeError>;
409
410    async fn get_gas_data_panic_if_not_gas(
411        &self,
412        gas_object_id: ObjectID,
413    ) -> (GasCoin, ObjectRef, Owner);
414}
415
416#[async_trait]
417impl IotaClientInner for IotaSdkClient {
418    type Error = iota_sdk::error::Error;
419
420    async fn query_events(
421        &self,
422        query: EventFilter,
423        cursor: Option<EventID>,
424    ) -> Result<EventPage, Self::Error> {
425        self.event_api()
426            .query_events(query, cursor, None, false)
427            .await
428    }
429
430    async fn get_events_by_tx_digest(
431        &self,
432        tx_digest: TransactionDigest,
433    ) -> Result<Vec<IotaEvent>, Self::Error> {
434        self.event_api().get_events(tx_digest).await
435    }
436
437    async fn get_chain_identifier(&self) -> Result<String, Self::Error> {
438        self.read_api().get_chain_identifier().await
439    }
440
441    async fn get_reference_gas_price(&self) -> Result<u64, Self::Error> {
442        self.governance_api().get_reference_gas_price().await
443    }
444
445    async fn get_latest_checkpoint_sequence_number(&self) -> Result<u64, Self::Error> {
446        self.read_api()
447            .get_latest_checkpoint_sequence_number()
448            .await
449    }
450
451    async fn get_mutable_bridge_object_arg(&self) -> Result<ObjectArg, Self::Error> {
452        let initial_shared_version = self
453            .http()
454            .get_bridge_object_initial_shared_version()
455            .await?;
456        Ok(ObjectArg::SharedObject {
457            id: IOTA_BRIDGE_OBJECT_ID,
458            initial_shared_version: SequenceNumber::from_u64(initial_shared_version),
459            mutable: true,
460        })
461    }
462
463    async fn get_bridge_summary(&self) -> Result<BridgeSummary, Self::Error> {
464        self.http().get_latest_bridge().await.map_err(|e| e.into())
465    }
466
467    async fn get_token_transfer_action_onchain_status(
468        &self,
469        bridge_object_arg: ObjectArg,
470        source_chain_id: u8,
471        seq_number: u64,
472    ) -> Result<BridgeActionStatus, BridgeError> {
473        dev_inspect_bridge::<u8>(
474            self,
475            bridge_object_arg,
476            source_chain_id,
477            seq_number,
478            "get_token_transfer_action_status",
479        )
480        .await
481        .and_then(|status_byte| BridgeActionStatus::try_from(status_byte).map_err(Into::into))
482    }
483
484    async fn get_token_transfer_action_onchain_signatures(
485        &self,
486        bridge_object_arg: ObjectArg,
487        source_chain_id: u8,
488        seq_number: u64,
489    ) -> Result<Option<Vec<Vec<u8>>>, BridgeError> {
490        dev_inspect_bridge::<Option<Vec<Vec<u8>>>>(
491            self,
492            bridge_object_arg,
493            source_chain_id,
494            seq_number,
495            "get_token_transfer_action_signatures",
496        )
497        .await
498    }
499
500    async fn execute_transaction_block_with_effects(
501        &self,
502        tx: Transaction,
503    ) -> Result<IotaTransactionBlockResponse, BridgeError> {
504        match self.quorum_driver_api().execute_transaction_block(
505            tx,
506            IotaTransactionBlockResponseOptions::new().with_effects().with_events(),
507            Some(iota_types::quorum_driver_types::ExecuteTransactionRequestType::WaitForEffectsCert),
508        ).await {
509            Ok(response) => Ok(response),
510            Err(e) => return Err(BridgeError::IotaTxFailureGeneric(e.to_string())),
511        }
512    }
513
514    async fn get_parsed_token_transfer_message(
515        &self,
516        bridge_object_arg: ObjectArg,
517        source_chain_id: u8,
518        seq_number: u64,
519    ) -> Result<Option<MoveTypeParsedTokenTransferMessage>, BridgeError> {
520        dev_inspect_bridge::<Option<MoveTypeParsedTokenTransferMessage>>(
521            self,
522            bridge_object_arg,
523            source_chain_id,
524            seq_number,
525            "get_parsed_token_transfer_message",
526        )
527        .await
528    }
529
530    async fn get_gas_data_panic_if_not_gas(
531        &self,
532        gas_object_id: ObjectID,
533    ) -> (GasCoin, ObjectRef, Owner) {
534        loop {
535            match self
536                .read_api()
537                .get_object_with_options(
538                    gas_object_id,
539                    IotaObjectDataOptions::default().with_owner().with_content(),
540                )
541                .await
542                .map(|resp| resp.data)
543            {
544                Ok(Some(gas_obj)) => {
545                    let owner = gas_obj.owner.expect("Owner is requested");
546                    let gas_coin = GasCoin::try_from(&gas_obj)
547                        .unwrap_or_else(|err| panic!("{} is not a gas coin: {err}", gas_object_id));
548                    return (gas_coin, gas_obj.object_ref(), owner);
549                }
550                other => {
551                    warn!("Can't get gas object: {:?}: {:?}", gas_object_id, other);
552                    tokio::time::sleep(Duration::from_secs(5)).await;
553                }
554            }
555        }
556    }
557}
558
559/// Helper function to dev-inspect `bridge::{function_name}` function
560/// with bridge object arg, source chain id, seq number as param
561/// and parse the return value as `T`.
562async fn dev_inspect_bridge<T>(
563    iota_client: &IotaSdkClient,
564    bridge_object_arg: ObjectArg,
565    source_chain_id: u8,
566    seq_number: u64,
567    function_name: &str,
568) -> Result<T, BridgeError>
569where
570    T: DeserializeOwned,
571{
572    let pt = ProgrammableTransaction {
573        inputs: vec![
574            CallArg::Object(bridge_object_arg),
575            CallArg::Pure(bcs::to_bytes(&source_chain_id).unwrap()),
576            CallArg::Pure(bcs::to_bytes(&seq_number).unwrap()),
577        ],
578        commands: vec![Command::MoveCall(Box::new(ProgrammableMoveCall {
579            package: BRIDGE_PACKAGE_ID,
580            module: Identifier::new("bridge").unwrap(),
581            function: Identifier::new(function_name).unwrap(),
582            type_arguments: vec![],
583            arguments: vec![Argument::Input(0), Argument::Input(1), Argument::Input(2)],
584        }))],
585    };
586    let kind = TransactionKind::programmable(pt);
587    let resp = iota_client
588        .read_api()
589        .dev_inspect_transaction_block(IotaAddress::ZERO, kind, None, None, None)
590        .await?;
591    let DevInspectResults {
592        results, effects, ..
593    } = resp;
594    let Some(results) = results else {
595        return Err(BridgeError::Generic(format!(
596            "No results returned for '{}', effects: {:?}",
597            function_name, effects
598        )));
599    };
600    let return_values = &results
601        .first()
602        .ok_or(BridgeError::Generic(format!(
603            "No return values for '{}', results: {:?}",
604            function_name, results
605        )))?
606        .return_values;
607    let (value_bytes, _type_tag) = return_values.first().ok_or(BridgeError::Generic(format!(
608        "No first return value for '{}', results: {:?}",
609        function_name, results
610    )))?;
611    bcs::from_bytes::<T>(value_bytes).map_err(|e| {
612        BridgeError::Generic(format!(
613            "Failed to parse return value for '{}', error: {:?}, results: {:?}",
614            function_name, e, results
615        ))
616    })
617}
618
619#[cfg(test)]
620mod tests {
621    use std::str::FromStr;
622
623    use ethers::types::Address as EthAddress;
624    use iota_json_rpc_types::BcsEvent;
625    use iota_types::{
626        bridge::{BridgeChainId, TOKEN_ID_IOTA, TOKEN_ID_USDC},
627        crypto::get_key_pair,
628    };
629    use move_core_types::account_address::AccountAddress;
630    use serde::{Deserialize, Serialize};
631    use test_cluster::TestClusterBuilder;
632
633    use super::*;
634    use crate::{
635        BRIDGE_ENABLE_PROTOCOL_VERSION,
636        crypto::BridgeAuthorityKeyPair,
637        events::{
638            EmittedIotaToEthTokenBridgeV1, IotaToEthTokenBridgeV1, MoveTokenDepositedEvent,
639            init_all_struct_tags,
640        },
641        iota_mock_client::IotaMockClient,
642        test_utils::{
643            approve_action_with_validator_secrets, bridge_token,
644            get_test_eth_to_iota_bridge_action, get_test_iota_to_eth_bridge_action,
645        },
646        types::IotaToEthBridgeAction,
647    };
648
649    #[tokio::test]
650    #[ignore = "https://github.com/iotaledger/iota/issues/3224"]
651    async fn get_bridge_action_by_tx_digest_and_event_idx_maybe() {
652        // Note: for random events generated in this test, we only care about
653        // tx_digest and event_seq, so it's ok that package and module does
654        // not match the query parameters.
655        telemetry_subscribers::init_for_testing();
656        let mock_client = IotaMockClient::default();
657        let iota_client = IotaClient::new_for_testing(mock_client.clone());
658        let tx_digest = TransactionDigest::random();
659
660        // Ensure all struct tags are inited
661        init_all_struct_tags();
662
663        let sanitized_event_1 = EmittedIotaToEthTokenBridgeV1 {
664            nonce: 1,
665            iota_chain_id: BridgeChainId::IotaTestnet,
666            iota_address: IotaAddress::random_for_testing_only(),
667            eth_chain_id: BridgeChainId::EthSepolia,
668            eth_address: EthAddress::random(),
669            token_id: TOKEN_ID_IOTA,
670            amount_iota_adjusted: 100,
671        };
672        let emitted_event_1 = MoveTokenDepositedEvent {
673            seq_num: sanitized_event_1.nonce,
674            source_chain: sanitized_event_1.iota_chain_id as u8,
675            sender_address: sanitized_event_1.iota_address.to_vec(),
676            target_chain: sanitized_event_1.eth_chain_id as u8,
677            target_address: sanitized_event_1.eth_address.as_bytes().to_vec(),
678            token_type: sanitized_event_1.token_id,
679            amount_iota_adjusted: sanitized_event_1.amount_iota_adjusted,
680        };
681
682        let mut iota_event_1 = IotaEvent::random_for_testing();
683        iota_event_1.type_ = IotaToEthTokenBridgeV1.get().unwrap().clone();
684        iota_event_1.bcs = BcsEvent::new(bcs::to_bytes(&emitted_event_1).unwrap());
685
686        #[derive(Serialize, Deserialize)]
687        struct RandomStruct {}
688
689        let event_2: RandomStruct = RandomStruct {};
690        // undeclared struct tag
691        let mut iota_event_2 = IotaEvent::random_for_testing();
692        iota_event_2.type_ = IotaToEthTokenBridgeV1.get().unwrap().clone();
693        iota_event_2.type_.module = Identifier::from_str("unrecognized_module").unwrap();
694        iota_event_2.bcs = BcsEvent::new(bcs::to_bytes(&event_2).unwrap());
695
696        // Event 3 is defined in non-bridge package
697        let mut iota_event_3 = iota_event_1.clone();
698        iota_event_3.type_.address = AccountAddress::random();
699
700        mock_client.add_events_by_tx_digest(
701            tx_digest,
702            vec![
703                iota_event_1.clone(),
704                iota_event_2.clone(),
705                iota_event_1.clone(),
706                iota_event_3.clone(),
707            ],
708        );
709        let expected_action_1 = BridgeAction::IotaToEthBridgeAction(IotaToEthBridgeAction {
710            iota_tx_digest: tx_digest,
711            iota_tx_event_index: 0,
712            iota_bridge_event: sanitized_event_1.clone(),
713        });
714        assert_eq!(
715            iota_client
716                .get_bridge_action_by_tx_digest_and_event_idx_maybe(&tx_digest, 0)
717                .await
718                .unwrap(),
719            expected_action_1,
720        );
721        let expected_action_2 = BridgeAction::IotaToEthBridgeAction(IotaToEthBridgeAction {
722            iota_tx_digest: tx_digest,
723            iota_tx_event_index: 2,
724            iota_bridge_event: sanitized_event_1.clone(),
725        });
726        assert_eq!(
727            iota_client
728                .get_bridge_action_by_tx_digest_and_event_idx_maybe(&tx_digest, 2)
729                .await
730                .unwrap(),
731            expected_action_2,
732        );
733        assert!(matches!(
734            iota_client
735                .get_bridge_action_by_tx_digest_and_event_idx_maybe(&tx_digest, 1)
736                .await
737                .unwrap_err(),
738            BridgeError::NoBridgeEventsInTxPosition
739        ),);
740        assert!(matches!(
741            iota_client
742                .get_bridge_action_by_tx_digest_and_event_idx_maybe(&tx_digest, 3)
743                .await
744                .unwrap_err(),
745            BridgeError::BridgeEventInUnrecognizedIotaPackage
746        ),);
747        assert!(matches!(
748            iota_client
749                .get_bridge_action_by_tx_digest_and_event_idx_maybe(&tx_digest, 4)
750                .await
751                .unwrap_err(),
752            BridgeError::NoBridgeEventsInTxPosition
753        ),);
754
755        // if the StructTag matches with unparsable bcs, it returns an error
756        iota_event_2.type_ = IotaToEthTokenBridgeV1.get().unwrap().clone();
757        mock_client.add_events_by_tx_digest(tx_digest, vec![iota_event_2]);
758        iota_client
759            .get_bridge_action_by_tx_digest_and_event_idx_maybe(&tx_digest, 2)
760            .await
761            .unwrap_err();
762    }
763
764    // Test get_action_onchain_status.
765    // Use validator secrets to bridge USDC from Ethereum initially.
766    // TODO: we need an e2e test for this with published solidity contract and
767    // committee with BridgeNodes
768    #[tokio::test(flavor = "multi_thread", worker_threads = 8)]
769    #[ignore = "https://github.com/iotaledger/iota/issues/3224"]
770    async fn test_get_action_onchain_status_for_iota_to_eth_transfer() {
771        telemetry_subscribers::init_for_testing();
772        let mut bridge_keys = vec![];
773        for _ in 0..=3 {
774            let (_, kp): (_, BridgeAuthorityKeyPair) = get_key_pair();
775            bridge_keys.push(kp);
776        }
777        let mut test_cluster: test_cluster::TestCluster = TestClusterBuilder::new()
778            .with_protocol_version((BRIDGE_ENABLE_PROTOCOL_VERSION).into())
779            .build_with_bridge(bridge_keys, true)
780            .await;
781
782        let iota_client = IotaClient::new(&test_cluster.fullnode_handle.rpc_url)
783            .await
784            .unwrap();
785        let bridge_authority_keys = test_cluster.bridge_authority_keys.take().unwrap();
786
787        // Wait until committee is set up
788        test_cluster
789            .trigger_reconfiguration_if_not_yet_and_assert_bridge_committee_initialized()
790            .await;
791        let context = &mut test_cluster.wallet;
792        let sender = context.active_address().unwrap();
793        let usdc_amount = 5000000;
794        let bridge_object_arg = iota_client
795            .get_mutable_bridge_object_arg_must_succeed()
796            .await;
797        let id_token_map = iota_client.get_token_id_map().await.unwrap();
798
799        // 1. Create a Eth -> IOTA Transfer (recipient is sender address), approve with
800        //    validator secrets and assert its status to be Claimed
801        let action =
802            get_test_eth_to_iota_bridge_action(None, Some(usdc_amount), Some(sender), None);
803        let usdc_object_ref = approve_action_with_validator_secrets(
804            context,
805            bridge_object_arg,
806            action.clone(),
807            &bridge_authority_keys,
808            Some(sender),
809            &id_token_map,
810        )
811        .await
812        .unwrap();
813
814        let status = iota_client
815            .inner
816            .get_token_transfer_action_onchain_status(
817                bridge_object_arg,
818                action.chain_id() as u8,
819                action.seq_number(),
820            )
821            .await
822            .unwrap();
823        assert_eq!(status, BridgeActionStatus::Claimed);
824
825        // 2. Create an IOTA -> Eth Transfer, approve with validator secrets and assert
826        //    its status to be Approved
827        // We need to actually send tokens to bridge to initialize the record.
828        let eth_recv_address = EthAddress::random();
829        let bridge_event = bridge_token(
830            context,
831            eth_recv_address,
832            usdc_object_ref,
833            id_token_map.get(&TOKEN_ID_USDC).unwrap().clone(),
834            bridge_object_arg,
835        )
836        .await;
837        assert_eq!(bridge_event.nonce, 0);
838        assert_eq!(bridge_event.iota_chain_id, BridgeChainId::IotaCustom);
839        assert_eq!(bridge_event.eth_chain_id, BridgeChainId::EthCustom);
840        assert_eq!(bridge_event.eth_address, eth_recv_address);
841        assert_eq!(bridge_event.iota_address, sender);
842        assert_eq!(bridge_event.token_id, TOKEN_ID_USDC);
843        assert_eq!(bridge_event.amount_iota_adjusted, usdc_amount);
844
845        let action = get_test_iota_to_eth_bridge_action(
846            None,
847            None,
848            Some(bridge_event.nonce),
849            Some(bridge_event.amount_iota_adjusted),
850            Some(bridge_event.iota_address),
851            Some(bridge_event.eth_address),
852            Some(TOKEN_ID_USDC),
853        );
854        let status = iota_client
855            .inner
856            .get_token_transfer_action_onchain_status(
857                bridge_object_arg,
858                action.chain_id() as u8,
859                action.seq_number(),
860            )
861            .await
862            .unwrap();
863        // At this point, the record is created and the status is Pending
864        assert_eq!(status, BridgeActionStatus::Pending);
865
866        // Approve it and assert its status to be Approved
867        approve_action_with_validator_secrets(
868            context,
869            bridge_object_arg,
870            action.clone(),
871            &bridge_authority_keys,
872            None,
873            &id_token_map,
874        )
875        .await;
876
877        let status = iota_client
878            .inner
879            .get_token_transfer_action_onchain_status(
880                bridge_object_arg,
881                action.chain_id() as u8,
882                action.seq_number(),
883            )
884            .await
885            .unwrap();
886        assert_eq!(status, BridgeActionStatus::Approved);
887
888        // 3. Create a random action and assert its status as NotFound
889        let action =
890            get_test_iota_to_eth_bridge_action(None, None, Some(100), None, None, None, None);
891        let status = iota_client
892            .inner
893            .get_token_transfer_action_onchain_status(
894                bridge_object_arg,
895                action.chain_id() as u8,
896                action.seq_number(),
897            )
898            .await
899            .unwrap();
900        assert_eq!(status, BridgeActionStatus::NotFound);
901    }
902}