iota_bridge/
utils.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 ethers::{
9    core::k256::ecdsa::SigningKey,
10    middleware::SignerMiddleware,
11    prelude::*,
12    providers::{Http, Provider},
13    signers::Wallet,
14    types::Address as EthAddress,
15};
16use fastcrypto::{
17    ed25519::Ed25519KeyPair,
18    encoding::{Encoding, Hex},
19    secp256k1::Secp256k1KeyPair,
20    traits::{EncodeDecodeBase64, KeyPair},
21};
22use futures::future::join_all;
23use iota_config::Config;
24use iota_json_rpc_types::{
25    IotaExecutionStatus, IotaTransactionBlockEffectsAPI, IotaTransactionBlockResponseOptions,
26};
27use iota_keys::keypair_file::read_key;
28use iota_sdk::wallet_context::WalletContext;
29use iota_test_transaction_builder::TestTransactionBuilder;
30use iota_types::{
31    BRIDGE_PACKAGE_ID,
32    base_types::IotaAddress,
33    bridge::{BRIDGE_MODULE_NAME, BRIDGE_REGISTER_FOREIGN_TOKEN_FUNCTION_NAME, BridgeChainId},
34    crypto::{IotaKeyPair, ToFromBytes, get_key_pair},
35    programmable_transaction_builder::ProgrammableTransactionBuilder,
36    transaction::{ObjectArg, TransactionData},
37};
38
39use crate::{
40    abi::{EthBridgeCommittee, EthBridgeConfig, EthBridgeLimiter, EthBridgeVault, EthIotaBridge},
41    config::{BridgeNodeConfig, EthConfig, IotaConfig},
42    crypto::{BridgeAuthorityKeyPair, BridgeAuthorityPublicKeyBytes},
43    server::APPLICATION_JSON,
44    types::{AddTokensOnIotaAction, BridgeAction},
45};
46
47pub type EthSigner = SignerMiddleware<Provider<Http>, Wallet<SigningKey>>;
48
49pub struct EthBridgeContracts<P> {
50    pub bridge: EthIotaBridge<Provider<P>>,
51    pub committee: EthBridgeCommittee<Provider<P>>,
52    pub limiter: EthBridgeLimiter<Provider<P>>,
53    pub vault: EthBridgeVault<Provider<P>>,
54    pub config: EthBridgeConfig<Provider<P>>,
55}
56
57/// Generate Bridge Authority key (Secp256k1KeyPair) and write to a file as
58/// base64 encoded `privkey`.
59pub fn generate_bridge_authority_key_and_write_to_file(
60    path: &PathBuf,
61) -> Result<(), anyhow::Error> {
62    let (_, kp): (_, BridgeAuthorityKeyPair) = get_key_pair();
63    let eth_address = BridgeAuthorityPublicKeyBytes::from(&kp.public).to_eth_address();
64    println!(
65        "Corresponding Ethereum address by this ecdsa key: {:?}",
66        eth_address
67    );
68    let iota_address = IotaAddress::from(&kp.public);
69    println!(
70        "Corresponding IOTA address by this ecdsa key: {:?}",
71        iota_address
72    );
73    let base64_encoded = kp.encode_base64();
74    std::fs::write(path, base64_encoded)
75        .map_err(|err| anyhow!("Failed to write encoded key to path: {:?}", err))
76}
77
78/// Generate Bridge Client key (Secp256k1KeyPair or Ed25519KeyPair) and write to
79/// a file as base64 encoded `flag || privkey`.
80pub fn generate_bridge_client_key_and_write_to_file(
81    path: &PathBuf,
82    use_ecdsa: bool,
83) -> Result<(), anyhow::Error> {
84    let kp = if use_ecdsa {
85        let (_, kp): (_, Secp256k1KeyPair) = get_key_pair();
86        let eth_address = BridgeAuthorityPublicKeyBytes::from(&kp.public).to_eth_address();
87        println!(
88            "Corresponding Ethereum address by this ecdsa key: {:?}",
89            eth_address
90        );
91        IotaKeyPair::from(kp)
92    } else {
93        let (_, kp): (_, Ed25519KeyPair) = get_key_pair();
94        IotaKeyPair::from(kp)
95    };
96    let iota_address = IotaAddress::from(&kp.public());
97    println!("Corresponding IOTA address by this key: {:?}", iota_address);
98
99    let contents = kp.encode_base64();
100    std::fs::write(path, contents)
101        .map_err(|err| anyhow!("Failed to write encoded key to path: {:?}", err))
102}
103
104/// Given the address of IotaBridge Proxy, return the addresses of the
105/// committee, limiter, vault, and config.
106pub async fn get_eth_contract_addresses<P: ethers::providers::JsonRpcClient + 'static>(
107    bridge_proxy_address: EthAddress,
108    provider: &Arc<Provider<P>>,
109) -> anyhow::Result<(EthAddress, EthAddress, EthAddress, EthAddress)> {
110    let iota_bridge = EthIotaBridge::new(bridge_proxy_address, provider.clone());
111    let committee_address: EthAddress = iota_bridge.committee().call().await?;
112    let limiter_address: EthAddress = iota_bridge.limiter().call().await?;
113    let vault_address: EthAddress = iota_bridge.vault().call().await?;
114    let committee = EthBridgeCommittee::new(committee_address, provider.clone());
115    let config_address: EthAddress = committee.config().call().await?;
116
117    Ok((
118        committee_address,
119        limiter_address,
120        vault_address,
121        config_address,
122    ))
123}
124
125/// Given the address of IotaBridge Proxy, return the contracts of the
126/// committee, limiter, vault, and config.
127pub async fn get_eth_contracts<P: ethers::providers::JsonRpcClient + 'static>(
128    bridge_proxy_address: EthAddress,
129    provider: &Arc<Provider<P>>,
130) -> anyhow::Result<EthBridgeContracts<P>> {
131    let iota_bridge = EthIotaBridge::new(bridge_proxy_address, provider.clone());
132    let committee_address: EthAddress = iota_bridge.committee().call().await?;
133    let limiter_address: EthAddress = iota_bridge.limiter().call().await?;
134    let vault_address: EthAddress = iota_bridge.vault().call().await?;
135    let committee = EthBridgeCommittee::new(committee_address, provider.clone());
136    let config_address: EthAddress = committee.config().call().await?;
137
138    let limiter = EthBridgeLimiter::new(limiter_address, provider.clone());
139    let vault = EthBridgeVault::new(vault_address, provider.clone());
140    let config = EthBridgeConfig::new(config_address, provider.clone());
141    Ok(EthBridgeContracts {
142        bridge: iota_bridge,
143        committee,
144        limiter,
145        vault,
146        config,
147    })
148}
149
150/// Read bridge key from a file and print the corresponding information.
151/// If `is_validator_key` is true, the key must be a Secp256k1 key.
152pub fn examine_key(path: &PathBuf, is_validator_key: bool) -> Result<(), anyhow::Error> {
153    let key = read_key(path, is_validator_key)?;
154    let iota_address = IotaAddress::from(&key.public());
155    let pubkey = match key {
156        IotaKeyPair::Secp256k1(kp) => {
157            println!("Secp256k1 key:");
158            let eth_address = BridgeAuthorityPublicKeyBytes::from(&kp.public).to_eth_address();
159            println!("Corresponding Ethereum address: {:x}", eth_address);
160            kp.public.as_bytes().to_vec()
161        }
162        IotaKeyPair::Ed25519(kp) => {
163            println!("Ed25519 key:");
164            kp.public().as_bytes().to_vec()
165        }
166        IotaKeyPair::Secp256r1(kp) => {
167            println!("Secp256r1 key:");
168            kp.public().as_bytes().to_vec()
169        }
170    };
171    println!("Corresponding IOTA address: {:?}", iota_address);
172    println!("Corresponding PublicKey: {:?}", Hex::encode(pubkey));
173    Ok(())
174}
175
176/// Generate Bridge Node Config template and write to a file.
177pub fn generate_bridge_node_config_and_write_to_file(
178    path: &PathBuf,
179    run_client: bool,
180) -> Result<(), anyhow::Error> {
181    let mut config = BridgeNodeConfig {
182        server_listen_port: 9191,
183        metrics_port: 9184,
184        bridge_authority_key_path: PathBuf::from("/path/to/your/bridge_authority_key"),
185        iota: IotaConfig {
186            iota_rpc_url: "your_iota_rpc_url".to_string(),
187            iota_bridge_chain_id: BridgeChainId::IotaTestnet as u8,
188            bridge_client_key_path: None,
189            bridge_client_gas_object: None,
190            iota_bridge_module_last_processed_event_id_override: None,
191        },
192        eth: EthConfig {
193            eth_rpc_url: "your_eth_rpc_url".to_string(),
194            eth_bridge_proxy_address: "0x0000000000000000000000000000000000000000".to_string(),
195            eth_bridge_chain_id: BridgeChainId::EthSepolia as u8,
196            eth_contracts_start_block_fallback: Some(0),
197            eth_contracts_start_block_override: None,
198        },
199        approved_governance_actions: vec![],
200        run_client,
201        db_path: None,
202    };
203    if run_client {
204        config.iota.bridge_client_key_path = Some(PathBuf::from("/path/to/your/bridge_client_key"));
205        config.db_path = Some(PathBuf::from("/path/to/your/client_db"));
206    }
207    config.save(path)
208}
209
210pub async fn get_eth_signer_client(url: &str, private_key_hex: &str) -> anyhow::Result<EthSigner> {
211    let provider = Provider::<Http>::try_from(url)
212        .unwrap()
213        .interval(std::time::Duration::from_millis(2000));
214    let chain_id = provider.get_chainid().await?;
215    let wallet = Wallet::from_str(private_key_hex)
216        .unwrap()
217        .with_chain_id(chain_id.as_u64());
218    Ok(SignerMiddleware::new(provider, wallet))
219}
220
221pub async fn publish_and_register_coins_return_add_coins_on_iota_action(
222    wallet_context: &WalletContext,
223    bridge_arg: ObjectArg,
224    token_packages_dir: Vec<PathBuf>,
225    token_ids: Vec<u8>,
226    token_prices: Vec<u64>,
227    nonce: u64,
228) -> BridgeAction {
229    assert!(token_ids.len() == token_packages_dir.len());
230    assert!(token_prices.len() == token_packages_dir.len());
231    let iota_client = wallet_context.get_client().await.unwrap();
232    let quorum_driver_api = Arc::new(iota_client.quorum_driver_api().clone());
233    let rgp = iota_client
234        .governance_api()
235        .get_reference_gas_price()
236        .await
237        .unwrap();
238
239    let senders = wallet_context.get_addresses();
240    // We want each sender to deal with one coin
241    assert!(senders.len() >= token_packages_dir.len());
242
243    // publish coin packages
244    let mut publish_tokens_tasks = vec![];
245
246    for (token_package_dir, sender) in token_packages_dir.iter().zip(senders.clone()) {
247        let gas = wallet_context
248            .get_one_gas_object_owned_by_address(sender)
249            .await
250            .unwrap()
251            .unwrap();
252        let tx = TestTransactionBuilder::new(sender, gas, rgp)
253            .publish(token_package_dir.to_path_buf())
254            .build();
255        let tx = wallet_context.sign_transaction(&tx);
256        let api_clone = quorum_driver_api.clone();
257        publish_tokens_tasks.push(tokio::spawn(async move {
258            api_clone.execute_transaction_block(
259                tx,
260                IotaTransactionBlockResponseOptions::new()
261                    .with_effects()
262                    .with_input()
263                    .with_events()
264                    .with_object_changes()
265                    .with_balance_changes(),
266                Some(iota_types::quorum_driver_types::ExecuteTransactionRequestType::WaitForLocalExecution),
267            ).await
268        }));
269    }
270    let publish_coin_responses = join_all(publish_tokens_tasks).await;
271
272    let mut token_type_names = vec![];
273    let mut register_tasks = vec![];
274    for (response, sender) in publish_coin_responses.into_iter().zip(senders.clone()) {
275        let response = response.unwrap().unwrap();
276        assert_eq!(
277            response.effects.unwrap().status(),
278            &IotaExecutionStatus::Success
279        );
280        let object_changes = response.object_changes.unwrap();
281        let mut tc = None;
282        let mut type_ = None;
283        let mut uc = None;
284        let mut metadata = None;
285        for object_change in &object_changes {
286            if let o @ iota_json_rpc_types::ObjectChange::Created { object_type, .. } =
287                object_change
288            {
289                if object_type.name.as_str().starts_with("TreasuryCap") {
290                    assert!(tc.is_none() && type_.is_none());
291                    tc = Some(o.clone());
292                    type_ = Some(object_type.type_params.first().unwrap().clone());
293                } else if object_type.name.as_str().starts_with("UpgradeCap") {
294                    assert!(uc.is_none());
295                    uc = Some(o.clone());
296                } else if object_type.name.as_str().starts_with("CoinMetadata") {
297                    assert!(metadata.is_none());
298                    metadata = Some(o.clone());
299                }
300            }
301        }
302        let (tc, type_, uc, metadata) =
303            (tc.unwrap(), type_.unwrap(), uc.unwrap(), metadata.unwrap());
304
305        // register with the bridge
306        let mut builder = ProgrammableTransactionBuilder::new();
307        let bridge_arg = builder.obj(bridge_arg).unwrap();
308        let uc_arg = builder
309            .obj(ObjectArg::ImmOrOwnedObject(uc.object_ref()))
310            .unwrap();
311        let tc_arg = builder
312            .obj(ObjectArg::ImmOrOwnedObject(tc.object_ref()))
313            .unwrap();
314        let metadata_arg = builder
315            .obj(ObjectArg::ImmOrOwnedObject(metadata.object_ref()))
316            .unwrap();
317        builder.programmable_move_call(
318            BRIDGE_PACKAGE_ID,
319            BRIDGE_MODULE_NAME.into(),
320            BRIDGE_REGISTER_FOREIGN_TOKEN_FUNCTION_NAME.into(),
321            vec![type_.clone()],
322            vec![bridge_arg, tc_arg, uc_arg, metadata_arg],
323        );
324        let pt = builder.finish();
325        let gas = wallet_context
326            .get_one_gas_object_owned_by_address(sender)
327            .await
328            .unwrap()
329            .unwrap();
330        let tx = TransactionData::new_programmable(sender, vec![gas], pt, 1_000_000_000, rgp);
331        let signed_tx = wallet_context.sign_transaction(&tx);
332        let api_clone = quorum_driver_api.clone();
333        register_tasks.push(async move {
334            api_clone
335                .execute_transaction_block(
336                    signed_tx,
337                    IotaTransactionBlockResponseOptions::new().with_effects(),
338                    None,
339                )
340                .await
341        });
342        token_type_names.push(type_);
343    }
344    for response in join_all(register_tasks).await {
345        assert_eq!(
346            response.unwrap().effects.unwrap().status(),
347            &IotaExecutionStatus::Success
348        );
349    }
350
351    BridgeAction::AddTokensOnIotaAction(AddTokensOnIotaAction {
352        nonce,
353        chain_id: BridgeChainId::IotaCustom,
354        native: false,
355        token_ids,
356        token_type_names,
357        token_prices,
358    })
359}
360
361pub async fn wait_for_server_to_be_up(server_url: String, timeout_sec: u64) -> anyhow::Result<()> {
362    let now = std::time::Instant::now();
363    loop {
364        if let Ok(true) = reqwest::Client::new()
365            .get(server_url.clone())
366            .header(reqwest::header::ACCEPT, APPLICATION_JSON)
367            .send()
368            .await
369            .map(|res| res.status().is_success())
370        {
371            break;
372        }
373        if now.elapsed().as_secs() > timeout_sec {
374            anyhow::bail!("Server is not up and running after {} seconds", timeout_sec);
375        }
376        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
377    }
378    Ok(())
379}