iota_bridge_cli/
lib.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{path::PathBuf, str::FromStr, sync::Arc};
6
7use anyhow::anyhow;
8use clap::*;
9use ethers::{
10    providers::Middleware,
11    types::{Address as EthAddress, U256},
12};
13use fastcrypto::{
14    encoding::{Encoding, Hex},
15    hash::{HashFunction, Keccak256},
16};
17use iota_bridge::{
18    abi::{EthBridgeCommittee, EthIotaBridge, eth_iota_bridge},
19    crypto::BridgeAuthorityPublicKeyBytes,
20    error::BridgeResult,
21    iota_client::IotaBridgeClient,
22    types::{
23        AddTokensOnEvmAction, AddTokensOnIotaAction, AssetPriceUpdateAction,
24        BlocklistCommitteeAction, BlocklistType, BridgeAction, EmergencyAction,
25        EmergencyActionType, EvmContractUpgradeAction, LimitUpdateAction,
26    },
27    utils::{EthSigner, get_eth_signer_client},
28};
29use iota_config::Config;
30use iota_json_rpc_types::IotaObjectDataOptions;
31use iota_keys::keypair_file::read_key;
32use iota_sdk::IotaClientBuilder;
33use iota_types::{
34    BRIDGE_PACKAGE_ID, TypeTag,
35    base_types::{IotaAddress, ObjectID, ObjectRef},
36    bridge::{BRIDGE_MODULE_NAME, BridgeChainId},
37    crypto::{IotaKeyPair, Signature},
38    programmable_transaction_builder::ProgrammableTransactionBuilder,
39    transaction::{ObjectArg, Transaction, TransactionData},
40};
41use move_core_types::ident_str;
42use serde::{Deserialize, Serialize};
43use serde_with::serde_as;
44use shared_crypto::intent::{Intent, IntentMessage};
45use tracing::info;
46
47pub const SEPOLIA_BRIDGE_PROXY_ADDR: &str = "0xAE68F87938439afEEDd6552B0E83D2CbC2473623";
48
49#[derive(Parser)]
50pub struct Args {
51    #[command(subcommand)]
52    pub command: BridgeCommand,
53}
54
55#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
56pub enum Network {
57    Testnet,
58}
59
60#[derive(Parser)]
61pub enum BridgeCommand {
62    CreateBridgeValidatorKey {
63        path: PathBuf,
64    },
65    CreateBridgeClientKey {
66        path: PathBuf,
67        #[arg(long, default_value = "false")]
68        use_ecdsa: bool,
69    },
70    /// Read bridge key from a file and print related information
71    /// If `is-validator-key` is true, the key must be a secp256k1 key
72    ExamineKey {
73        path: PathBuf,
74        #[arg(long)]
75        is_validator_key: bool,
76    },
77    CreateBridgeNodeConfigTemplate {
78        path: PathBuf,
79        #[arg(long)]
80        run_client: bool,
81    },
82    /// Governance client to facilitate and execute Bridge governance actions
83    Governance {
84        /// Path of BridgeCliConfig
85        #[arg(long)]
86        config_path: PathBuf,
87        #[arg(long)]
88        chain_id: u8,
89        #[command(subcommand)]
90        cmd: GovernanceClientCommands,
91        /// If true, only collect signatures but not execute on chain
92        #[arg(long)]
93        dry_run: bool,
94    },
95    /// View current status of Eth bridge
96    ViewEthBridge {
97        #[arg(long)]
98        network: Option<Network>,
99        #[arg(long)]
100        bridge_proxy: Option<EthAddress>,
101        #[arg(long)]
102        eth_rpc_url: String,
103    },
104    /// View current list of registered validators
105    ViewBridgeRegistration {
106        #[arg(long)]
107        iota_rpc_url: String,
108    },
109    /// View current status of IOTA bridge
110    ViewIotaBridge {
111        #[arg(long)]
112        iota_rpc_url: String,
113        #[arg(long, default_value = "false")]
114        hex: bool,
115        #[arg(long, default_value = "false")]
116        ping: bool,
117    },
118    /// Client to facilitate and execute Bridge actions
119    Client {
120        /// Path of BridgeCliConfig
121        #[arg(long)]
122        config_path: PathBuf,
123        #[command(subcommand)]
124        cmd: BridgeClientCommands,
125    },
126}
127
128#[derive(Parser)]
129pub enum GovernanceClientCommands {
130    EmergencyButton {
131        #[arg(name = "nonce", long)]
132        nonce: u64,
133        #[arg(name = "action-type", long)]
134        action_type: EmergencyActionType,
135    },
136    UpdateCommitteeBlocklist {
137        #[arg(name = "nonce", long)]
138        nonce: u64,
139        #[arg(name = "blocklist-type", long)]
140        blocklist_type: BlocklistType,
141        #[arg(name = "pubkey-hex", use_value_delimiter = true, long)]
142        pubkeys_hex: Vec<BridgeAuthorityPublicKeyBytes>,
143    },
144    UpdateLimit {
145        #[arg(name = "nonce", long)]
146        nonce: u64,
147        #[arg(name = "sending-chain", long)]
148        sending_chain: u8,
149        #[arg(name = "new-usd-limit", long)]
150        new_usd_limit: u64,
151    },
152    UpdateAssetPrice {
153        #[arg(name = "nonce", long)]
154        nonce: u64,
155        #[arg(name = "token-id", long)]
156        token_id: u8,
157        #[arg(name = "new-usd-price", long)]
158        new_usd_price: u64,
159    },
160    AddTokensOnIota {
161        #[arg(name = "nonce", long)]
162        nonce: u64,
163        #[arg(name = "token-ids", use_value_delimiter = true, long)]
164        token_ids: Vec<u8>,
165        #[arg(name = "token-type-names", use_value_delimiter = true, long)]
166        token_type_names: Vec<TypeTag>,
167        #[arg(name = "token-prices", use_value_delimiter = true, long)]
168        token_prices: Vec<u64>,
169    },
170    AddTokensOnEvm {
171        #[arg(name = "nonce", long)]
172        nonce: u64,
173        #[arg(name = "token-ids", use_value_delimiter = true, long)]
174        token_ids: Vec<u8>,
175        #[arg(name = "token-type-names", use_value_delimiter = true, long)]
176        token_addresses: Vec<EthAddress>,
177        #[arg(name = "token-prices", use_value_delimiter = true, long)]
178        token_prices: Vec<u64>,
179        #[arg(name = "token-iota-decimals", use_value_delimiter = true, long)]
180        token_iota_decimals: Vec<u8>,
181    },
182    #[command(name = "upgrade-evm-contract")]
183    UpgradeEVMContract {
184        #[arg(name = "nonce", long)]
185        nonce: u64,
186        #[arg(name = "proxy-address", long)]
187        proxy_address: EthAddress,
188        /// The address of the new implementation contract
189        #[arg(name = "implementation-address", long)]
190        implementation_address: EthAddress,
191        /// Function selector with params types, e.g. `foo(uint256,bool,string)`
192        #[arg(name = "function-selector", long)]
193        function_selector: Option<String>,
194        /// Params to be passed to the function, e.g. `420,false,hello`
195        #[arg(name = "params", use_value_delimiter = true, long)]
196        params: Vec<String>,
197    },
198}
199
200pub fn make_action(chain_id: BridgeChainId, cmd: &GovernanceClientCommands) -> BridgeAction {
201    match cmd {
202        GovernanceClientCommands::EmergencyButton { nonce, action_type } => {
203            BridgeAction::EmergencyAction(EmergencyAction {
204                nonce: *nonce,
205                chain_id,
206                action_type: *action_type,
207            })
208        }
209        GovernanceClientCommands::UpdateCommitteeBlocklist {
210            nonce,
211            blocklist_type,
212            pubkeys_hex,
213        } => BridgeAction::BlocklistCommitteeAction(BlocklistCommitteeAction {
214            nonce: *nonce,
215            chain_id,
216            blocklist_type: *blocklist_type,
217            members_to_update: pubkeys_hex.clone(),
218        }),
219        GovernanceClientCommands::UpdateLimit {
220            nonce,
221            sending_chain,
222            new_usd_limit,
223        } => {
224            let sending_chain_id =
225                BridgeChainId::try_from(*sending_chain).expect("Invalid sending chain id");
226            BridgeAction::LimitUpdateAction(LimitUpdateAction {
227                nonce: *nonce,
228                chain_id,
229                sending_chain_id,
230                new_usd_limit: *new_usd_limit,
231            })
232        }
233        GovernanceClientCommands::UpdateAssetPrice {
234            nonce,
235            token_id,
236            new_usd_price,
237        } => BridgeAction::AssetPriceUpdateAction(AssetPriceUpdateAction {
238            nonce: *nonce,
239            chain_id,
240            token_id: *token_id,
241            new_usd_price: *new_usd_price,
242        }),
243        GovernanceClientCommands::AddTokensOnIota {
244            nonce,
245            token_ids,
246            token_type_names,
247            token_prices,
248        } => {
249            assert_eq!(token_ids.len(), token_type_names.len());
250            assert_eq!(token_ids.len(), token_prices.len());
251            BridgeAction::AddTokensOnIotaAction(AddTokensOnIotaAction {
252                nonce: *nonce,
253                chain_id,
254                native: false, // only foreign tokens are supported now
255                token_ids: token_ids.clone(),
256                token_type_names: token_type_names.clone(),
257                token_prices: token_prices.clone(),
258            })
259        }
260        GovernanceClientCommands::AddTokensOnEvm {
261            nonce,
262            token_ids,
263            token_addresses,
264            token_prices,
265            token_iota_decimals,
266        } => {
267            assert_eq!(token_ids.len(), token_addresses.len());
268            assert_eq!(token_ids.len(), token_prices.len());
269            assert_eq!(token_ids.len(), token_iota_decimals.len());
270            BridgeAction::AddTokensOnEvmAction(AddTokensOnEvmAction {
271                nonce: *nonce,
272                native: true, // only eth native tokens are supported now
273                chain_id,
274                token_ids: token_ids.clone(),
275                token_addresses: token_addresses.clone(),
276                token_prices: token_prices.clone(),
277                token_iota_decimals: token_iota_decimals.clone(),
278            })
279        }
280        GovernanceClientCommands::UpgradeEVMContract {
281            nonce,
282            proxy_address,
283            implementation_address,
284            function_selector,
285            params,
286        } => {
287            let call_data = match function_selector {
288                Some(function_selector) => encode_call_data(function_selector, params),
289                None => vec![],
290            };
291            BridgeAction::EvmContractUpgradeAction(EvmContractUpgradeAction {
292                nonce: *nonce,
293                chain_id,
294                proxy_address: *proxy_address,
295                new_impl_address: *implementation_address,
296                call_data,
297            })
298        }
299    }
300}
301
302fn encode_call_data(function_selector: &str, params: &[String]) -> Vec<u8> {
303    let left = function_selector
304        .find('(')
305        .expect("Invalid function selector, no left parentheses");
306    let right = function_selector
307        .find(')')
308        .expect("Invalid function selector, no right parentheses");
309    let param_types = function_selector[left + 1..right]
310        .split(',')
311        .map(|x| x.trim())
312        .collect::<Vec<&str>>();
313
314    assert_eq!(param_types.len(), params.len(), "Invalid number of params");
315
316    let mut call_data = Keccak256::digest(function_selector).digest[0..4].to_vec();
317    let mut tokens = vec![];
318    for (param, param_type) in params.iter().zip(param_types.iter()) {
319        match param_type.to_lowercase().as_str() {
320            "uint256" => {
321                tokens.push(ethers::abi::Token::Uint(
322                    ethers::types::U256::from_dec_str(param).expect("Invalid U256"),
323                ));
324            }
325            "bool" => {
326                tokens.push(ethers::abi::Token::Bool(match param.as_str() {
327                    "true" => true,
328                    "false" => false,
329                    _ => panic!("Invalid bool in params"),
330                }));
331            }
332            "string" => {
333                tokens.push(ethers::abi::Token::String(param.clone()));
334            }
335            // TODO: need to support more types if needed
336            _ => panic!("Invalid param type"),
337        }
338    }
339    if !tokens.is_empty() {
340        call_data.extend(ethers::abi::encode(&tokens));
341    }
342    call_data
343}
344
345pub fn select_contract_address(
346    config: &LoadedBridgeCliConfig,
347    cmd: &GovernanceClientCommands,
348) -> EthAddress {
349    match cmd {
350        GovernanceClientCommands::EmergencyButton { .. } => config.eth_bridge_proxy_address,
351        GovernanceClientCommands::UpdateCommitteeBlocklist { .. } => {
352            config.eth_bridge_committee_proxy_address
353        }
354        GovernanceClientCommands::UpdateLimit { .. } => config.eth_bridge_limiter_proxy_address,
355        GovernanceClientCommands::UpdateAssetPrice { .. } => config.eth_bridge_config_proxy_address,
356        GovernanceClientCommands::UpgradeEVMContract { proxy_address, .. } => *proxy_address,
357        GovernanceClientCommands::AddTokensOnIota { .. } => unreachable!(),
358        GovernanceClientCommands::AddTokensOnEvm { .. } => config.eth_bridge_config_proxy_address,
359    }
360}
361
362#[serde_as]
363#[derive(Clone, Debug, Deserialize, Serialize)]
364#[serde(rename_all = "kebab-case")]
365pub struct BridgeCliConfig {
366    /// Rpc url for IOTA fullnode, used for query stuff and submit transactions.
367    pub iota_rpc_url: String,
368    /// Rpc url for Eth fullnode, used for query stuff.
369    pub eth_rpc_url: String,
370    /// Proxy address for IotaBridge deployed on Eth
371    pub eth_bridge_proxy_address: EthAddress,
372    /// Path of the file where private key is stored. The content could be any
373    /// of the following:
374    /// - Base64 encoded `flag || privkey` for ECDSA key
375    /// - Base64 encoded `privkey` for Raw key
376    /// - Hex encoded `privkey` for Raw key
377    /// At least one of `iota_key_path` or `eth_key_path` must be provided.
378    /// If only one is provided, it will be used for both IOTA and Eth.
379    pub iota_key_path: Option<PathBuf>,
380    /// See `iota_key_path`. Must be Secp256k1 key.
381    pub eth_key_path: Option<PathBuf>,
382}
383
384impl Config for BridgeCliConfig {}
385
386pub struct LoadedBridgeCliConfig {
387    /// Rpc url for IOTA fullnode, used for query stuff and submit transactions.
388    pub iota_rpc_url: String,
389    /// Rpc url for Eth fullnode, used for query stuff.
390    pub eth_rpc_url: String,
391    /// Proxy address for IotaBridge deployed on Eth
392    pub eth_bridge_proxy_address: EthAddress,
393    /// Proxy address for BridgeCommittee deployed on Eth
394    pub eth_bridge_committee_proxy_address: EthAddress,
395    /// Proxy address for BridgeConfig deployed on Eth
396    pub eth_bridge_config_proxy_address: EthAddress,
397    /// Proxy address for BridgeLimiter deployed on Eth
398    pub eth_bridge_limiter_proxy_address: EthAddress,
399    /// Key pair for IOTA operations
400    iota_key: IotaKeyPair,
401    /// Key pair for Eth operations, must be Secp256k1 key
402    eth_signer: EthSigner,
403}
404
405impl LoadedBridgeCliConfig {
406    pub async fn load(cli_config: BridgeCliConfig) -> anyhow::Result<Self> {
407        if cli_config.eth_key_path.is_none() && cli_config.iota_key_path.is_none() {
408            return Err(anyhow!(
409                "At least one of `iota_key_path` or `eth_key_path` must be provided"
410            ));
411        }
412        let iota_key = if let Some(iota_key_path) = &cli_config.iota_key_path {
413            Some(read_key(iota_key_path, false)?)
414        } else {
415            None
416        };
417        let eth_key = if let Some(eth_key_path) = &cli_config.eth_key_path {
418            let eth_key = read_key(eth_key_path, true)?;
419            Some(eth_key)
420        } else {
421            None
422        };
423        let (eth_key, iota_key) = {
424            if eth_key.is_none() {
425                let iota_key = iota_key.unwrap();
426                if !matches!(iota_key, IotaKeyPair::Secp256k1(_)) {
427                    return Err(anyhow!("Eth key must be an ECDSA key"));
428                }
429                (iota_key.copy(), iota_key)
430            } else if iota_key.is_none() {
431                let eth_key = eth_key.unwrap();
432                (eth_key.copy(), eth_key)
433            } else {
434                (eth_key.unwrap(), iota_key.unwrap())
435            }
436        };
437
438        let provider = Arc::new(
439            ethers::prelude::Provider::<ethers::providers::Http>::try_from(&cli_config.eth_rpc_url)
440                .unwrap()
441                .interval(std::time::Duration::from_millis(2000)),
442        );
443        let private_key = Hex::encode(eth_key.to_bytes_no_flag());
444        let eth_signer = get_eth_signer_client(&cli_config.eth_rpc_url, &private_key).await?;
445        let iota_bridge = EthIotaBridge::new(cli_config.eth_bridge_proxy_address, provider.clone());
446        let eth_bridge_committee_proxy_address: EthAddress = iota_bridge.committee().call().await?;
447        let eth_bridge_limiter_proxy_address: EthAddress = iota_bridge.limiter().call().await?;
448        let eth_committee =
449            EthBridgeCommittee::new(eth_bridge_committee_proxy_address, provider.clone());
450        let eth_bridge_committee_proxy_address: EthAddress = iota_bridge.committee().call().await?;
451        let eth_bridge_config_proxy_address: EthAddress = eth_committee.config().call().await?;
452
453        let eth_address = eth_signer.address();
454        let eth_chain_id = provider.get_chainid().await?;
455        let iota_address = IotaAddress::from(&iota_key.public());
456        println!("Using IOTA address: {:?}", iota_address);
457        println!("Using Eth address: {:?}", eth_address);
458        println!("Using Eth chain: {:?}", eth_chain_id);
459
460        Ok(Self {
461            iota_rpc_url: cli_config.iota_rpc_url,
462            eth_rpc_url: cli_config.eth_rpc_url,
463            eth_bridge_proxy_address: cli_config.eth_bridge_proxy_address,
464            eth_bridge_committee_proxy_address,
465            eth_bridge_limiter_proxy_address,
466            eth_bridge_config_proxy_address,
467            iota_key,
468            eth_signer,
469        })
470    }
471}
472
473impl LoadedBridgeCliConfig {
474    pub fn eth_signer(self: &LoadedBridgeCliConfig) -> &EthSigner {
475        &self.eth_signer
476    }
477
478    pub async fn get_iota_account_info(
479        self: &LoadedBridgeCliConfig,
480    ) -> anyhow::Result<(IotaKeyPair, IotaAddress, ObjectRef)> {
481        let pubkey = self.iota_key.public();
482        let iota_client_address = IotaAddress::from(&pubkey);
483        let iota_sdk_client = IotaClientBuilder::default()
484            .build(self.iota_rpc_url.clone())
485            .await?;
486        let gases = iota_sdk_client
487            .coin_read_api()
488            .get_coins(iota_client_address, None, None, None)
489            .await?
490            .data;
491        // TODO: is 5 IOTA a good number?
492        let gas = gases
493            .into_iter()
494            .find(|coin| coin.balance >= 5_000_000_000)
495            .ok_or(anyhow!(
496                "Did not find gas object with enough balance for {}",
497                iota_client_address
498            ))?;
499        println!("Using Gas object: {}", gas.coin_object_id);
500        Ok((self.iota_key.copy(), iota_client_address, gas.object_ref()))
501    }
502}
503#[derive(Parser)]
504pub enum BridgeClientCommands {
505    DepositNativeEtherOnEth {
506        #[arg(long)]
507        ether_amount: f64,
508        #[arg(long)]
509        target_chain: u8,
510        #[arg(long)]
511        iota_recipient_address: IotaAddress,
512    },
513    DepositOnIota {
514        #[arg(long)]
515        coin_object_id: ObjectID,
516        #[arg(long)]
517        coin_type: String,
518        #[arg(long)]
519        target_chain: u8,
520        #[arg(long)]
521        recipient_address: EthAddress,
522    },
523    ClaimOnEth {
524        #[arg(long)]
525        seq_num: u64,
526    },
527}
528
529impl BridgeClientCommands {
530    pub async fn handle(
531        self,
532        config: &LoadedBridgeCliConfig,
533        iota_bridge_client: IotaBridgeClient,
534    ) -> anyhow::Result<()> {
535        match self {
536            BridgeClientCommands::DepositNativeEtherOnEth {
537                ether_amount,
538                target_chain,
539                iota_recipient_address,
540            } => {
541                let eth_iota_bridge = EthIotaBridge::new(
542                    config.eth_bridge_proxy_address,
543                    Arc::new(config.eth_signer().clone()),
544                );
545                // Note: even with f64 there may still be loss of precision even there are a lot
546                // of 0s
547                let int_part = ether_amount.trunc() as u64;
548                let frac_part = ether_amount.fract();
549                let int_wei = U256::from(int_part) * U256::exp10(18);
550                let frac_wei = U256::from((frac_part * 1_000_000_000_000_000_000f64) as u64);
551                let amount = int_wei + frac_wei;
552                let eth_tx = eth_iota_bridge
553                    .bridge_eth(iota_recipient_address.to_vec().into(), target_chain)
554                    .value(amount);
555                let pending_tx = eth_tx.send().await.unwrap();
556                let tx_receipt = pending_tx.await.unwrap().unwrap();
557                info!(
558                    "Deposited {ether_amount} Ethers to {:?} (target chain {target_chain}). Receipt: {:?}",
559                    iota_recipient_address, tx_receipt,
560                );
561                Ok(())
562            }
563            BridgeClientCommands::ClaimOnEth { seq_num } => {
564                claim_on_eth(seq_num, config, iota_bridge_client)
565                    .await
566                    .map_err(|e| anyhow!("{:?}", e))
567            }
568            BridgeClientCommands::DepositOnIota {
569                coin_object_id,
570                coin_type,
571                target_chain,
572                recipient_address,
573            } => {
574                let target_chain = BridgeChainId::try_from(target_chain).expect("Invalid chain id");
575                let coin_type = TypeTag::from_str(&coin_type).expect("Invalid coin type");
576                deposit_on_iota(
577                    coin_object_id,
578                    coin_type,
579                    target_chain,
580                    recipient_address,
581                    config,
582                    iota_bridge_client,
583                )
584                .await
585            }
586        }
587    }
588}
589
590async fn deposit_on_iota(
591    coin_object_id: ObjectID,
592    coin_type: TypeTag,
593    target_chain: BridgeChainId,
594    recipient_address: EthAddress,
595    config: &LoadedBridgeCliConfig,
596    iota_bridge_client: IotaBridgeClient,
597) -> anyhow::Result<()> {
598    let target_chain = target_chain as u8;
599    let iota_client = iota_bridge_client.iota_client();
600    let bridge_object_arg = iota_bridge_client
601        .get_mutable_bridge_object_arg_must_succeed()
602        .await;
603    let rgp = iota_client
604        .governance_api()
605        .get_reference_gas_price()
606        .await
607        .unwrap();
608    let sender = IotaAddress::from(&config.iota_key.public());
609    let gas_obj_ref = iota_client
610        .coin_read_api()
611        .select_coins(sender, None, 1_000_000_000, vec![])
612        .await?
613        .first()
614        .ok_or(anyhow!("No coin found for address {}", sender))?
615        .object_ref();
616    let coin_obj_ref = iota_client
617        .read_api()
618        .get_object_with_options(coin_object_id, IotaObjectDataOptions::default())
619        .await?
620        .data
621        .unwrap()
622        .object_ref();
623
624    let mut builder = ProgrammableTransactionBuilder::new();
625    let arg_target_chain = builder.pure(target_chain).unwrap();
626    let arg_target_address = builder.pure(recipient_address.as_bytes()).unwrap();
627    let arg_token = builder
628        .obj(ObjectArg::ImmOrOwnedObject(coin_obj_ref))
629        .unwrap();
630    let arg_bridge = builder.obj(bridge_object_arg).unwrap();
631
632    builder.programmable_move_call(
633        BRIDGE_PACKAGE_ID,
634        BRIDGE_MODULE_NAME.to_owned(),
635        ident_str!("send_token").to_owned(),
636        vec![coin_type],
637        vec![arg_bridge, arg_target_chain, arg_target_address, arg_token],
638    );
639    let pt = builder.finish();
640    let tx_data =
641        TransactionData::new_programmable(sender, vec![gas_obj_ref], pt, 500_000_000, rgp);
642    let sig = Signature::new_secure(
643        &IntentMessage::new(Intent::iota_transaction(), tx_data.clone()),
644        &config.iota_key,
645    );
646    let signed_tx = Transaction::from_data(tx_data, vec![sig]);
647    let tx_digest = *signed_tx.digest();
648    info!(?tx_digest, "Sending deposit transaction to IOTA.");
649    let resp = iota_bridge_client
650        .execute_transaction_block_with_effects(signed_tx)
651        .await
652        .expect("Failed to execute transaction block");
653    if !resp.status_ok().unwrap() {
654        return Err(anyhow!("Transaction {:?} failed: {:?}", tx_digest, resp));
655    }
656    let events = resp.events.unwrap();
657    info!(
658        ?tx_digest,
659        "Deposit transaction succeeded. Events: {:?}", events
660    );
661    Ok(())
662}
663
664async fn claim_on_eth(
665    seq_num: u64,
666    config: &LoadedBridgeCliConfig,
667    iota_bridge_client: IotaBridgeClient,
668) -> BridgeResult<()> {
669    let iota_chain_id = iota_bridge_client.get_bridge_summary().await?.chain_id;
670    let parsed_message = iota_bridge_client
671        .get_parsed_token_transfer_message(iota_chain_id, seq_num)
672        .await?;
673    if parsed_message.is_none() {
674        println!("No record found for seq_num: {seq_num}, chain id: {iota_chain_id}");
675        return Ok(());
676    }
677    let parsed_message = parsed_message.unwrap();
678    let sigs = iota_bridge_client
679        .get_token_transfer_action_onchain_signatures_until_success(iota_chain_id, seq_num)
680        .await;
681    if sigs.is_none() {
682        println!("No signatures found for seq_num: {seq_num}, chain id: {iota_chain_id}");
683        return Ok(());
684    }
685    let signatures = sigs
686        .unwrap()
687        .into_iter()
688        .map(|sig: Vec<u8>| ethers::types::Bytes::from(sig))
689        .collect::<Vec<_>>();
690
691    let eth_iota_bridge = EthIotaBridge::new(
692        config.eth_bridge_proxy_address,
693        Arc::new(config.eth_signer().clone()),
694    );
695    let message = eth_iota_bridge::Message::from(parsed_message);
696    let tx = eth_iota_bridge.transfer_bridged_tokens_with_signatures(signatures, message);
697    let _eth_claim_tx_receipt = tx.send().await.unwrap().await.unwrap().unwrap();
698    info!("IOTA to Eth bridge transfer claimed");
699    Ok(())
700}
701
702#[cfg(test)]
703mod tests {
704    use ethers::abi::FunctionExt;
705
706    use super::*;
707
708    #[tokio::test]
709    #[ignore = "https://github.com/iotaledger/iota/issues/3224"]
710    async fn test_encode_call_data() {
711        let abi_json =
712            std::fs::read_to_string("../iota-bridge/abi/tests/mock_iota_bridge_v2.json").unwrap();
713        let abi: ethers::abi::Abi = serde_json::from_str(&abi_json).unwrap();
714
715        let function_selector = "initializeV2Params(uint256,bool,string)";
716        let params = vec!["420".to_string(), "false".to_string(), "hello".to_string()];
717        let call_data = encode_call_data(function_selector, &params);
718
719        let function = abi
720            .functions()
721            .find(|f| {
722                let selector = f.selector();
723                call_data.starts_with(selector.as_ref())
724            })
725            .expect("Function not found");
726
727        // Decode the data excluding the selector
728        let tokens = function.decode_input(&call_data[4..]).unwrap();
729        assert_eq!(
730            tokens,
731            vec![
732                ethers::abi::Token::Uint(ethers::types::U256::from_dec_str("420").unwrap()),
733                ethers::abi::Token::Bool(false),
734                ethers::abi::Token::String("hello".to_string())
735            ]
736        )
737    }
738}