sui_bridge/
utils.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::abi::{
5    EthBridgeCommittee, EthBridgeConfig, EthBridgeLimiter, EthBridgeVault, EthSuiBridge,
6};
7use crate::config::{
8    BridgeNodeConfig, EthConfig, MetricsConfig, SuiConfig, WatchdogConfig, default_ed25519_key_pair,
9};
10use crate::crypto::{BridgeAuthorityKeyPair, BridgeAuthorityPublicKeyBytes};
11use crate::server::APPLICATION_JSON;
12use crate::types::{AddTokensOnSuiAction, BridgeAction, BridgeCommittee};
13use alloy::network::EthereumWallet;
14use alloy::primitives::Address as EthAddress;
15use alloy::providers::{ProviderBuilder, RootProvider, WsConnect};
16use alloy::signers::local::PrivateKeySigner;
17use anyhow::anyhow;
18use fastcrypto::ed25519::Ed25519KeyPair;
19use fastcrypto::encoding::{Encoding, Hex};
20use fastcrypto::secp256k1::Secp256k1KeyPair;
21use fastcrypto::traits::{EncodeDecodeBase64, KeyPair};
22use futures::future::join_all;
23use std::collections::BTreeMap;
24use std::path::PathBuf;
25use std::str::FromStr;
26use std::sync::Arc;
27use sui_config::Config;
28use sui_json_rpc_types::{
29    SuiExecutionStatus, SuiTransactionBlockEffectsAPI, SuiTransactionBlockResponseOptions,
30};
31use sui_keys::keypair_file::read_key;
32use sui_sdk::wallet_context::WalletContext;
33use sui_test_transaction_builder::TestTransactionBuilder;
34use sui_types::BRIDGE_PACKAGE_ID;
35use sui_types::base_types::SuiAddress;
36use sui_types::bridge::{
37    BRIDGE_MODULE_NAME, BRIDGE_REGISTER_FOREIGN_TOKEN_FUNCTION_NAME, BridgeChainId,
38};
39use sui_types::committee::StakeUnit;
40use sui_types::crypto::{SuiKeyPair, ToFromBytes, get_key_pair};
41use sui_types::programmable_transaction_builder::ProgrammableTransactionBuilder;
42use sui_types::sui_system_state::sui_system_state_summary::SuiSystemStateSummary;
43use sui_types::transaction::{ObjectArg, TransactionData};
44use url::Url;
45
46pub struct EthBridgeContracts {
47    pub bridge: EthSuiBridge::EthSuiBridgeInstance<EthProvider>,
48    pub committee: EthBridgeCommittee::EthBridgeCommitteeInstance<EthProvider>,
49    pub limiter: EthBridgeLimiter::EthBridgeLimiterInstance<EthProvider>,
50    pub vault: EthBridgeVault::EthBridgeVaultInstance<EthProvider>,
51    pub config: EthBridgeConfig::EthBridgeConfigInstance<EthProvider>,
52}
53
54pub type EthProvider = Arc<RootProvider<alloy::network::Ethereum>>;
55pub type EthSignerProvider = Arc<
56    alloy::providers::fillers::FillProvider<
57        alloy::providers::fillers::JoinFill<
58            alloy::providers::fillers::JoinFill<
59                alloy::providers::Identity,
60                alloy::providers::fillers::JoinFill<
61                    alloy::providers::fillers::GasFiller,
62                    alloy::providers::fillers::JoinFill<
63                        alloy::providers::fillers::BlobGasFiller,
64                        alloy::providers::fillers::JoinFill<
65                            alloy::providers::fillers::NonceFiller,
66                            alloy::providers::fillers::ChainIdFiller,
67                        >,
68                    >,
69                >,
70            >,
71            alloy::providers::fillers::WalletFiller<EthereumWallet>,
72        >,
73        EthProvider,
74        alloy::network::Ethereum,
75    >,
76>;
77pub type EthWsProvider = Arc<
78    alloy::providers::fillers::FillProvider<
79        alloy::providers::fillers::JoinFill<
80            alloy::providers::Identity,
81            alloy::providers::fillers::JoinFill<
82                alloy::providers::fillers::GasFiller,
83                alloy::providers::fillers::JoinFill<
84                    alloy::providers::fillers::BlobGasFiller,
85                    alloy::providers::fillers::JoinFill<
86                        alloy::providers::fillers::NonceFiller,
87                        alloy::providers::fillers::ChainIdFiller,
88                    >,
89                >,
90            >,
91        >,
92        alloy::providers::RootProvider<alloy::network::Ethereum>,
93        alloy::network::Ethereum,
94    >,
95>;
96
97pub fn get_eth_provider(url: &str) -> anyhow::Result<EthProvider> {
98    let url = Url::parse(url).map_err(|e| anyhow!("Invalid RPC URL: {}", e))?;
99    let provider = RootProvider::new_http(url);
100    Ok(Arc::new(provider))
101}
102
103pub fn get_eth_signer_provider(
104    url: &str,
105    private_key_hex: &str,
106) -> anyhow::Result<EthSignerProvider> {
107    let signer = PrivateKeySigner::from_str(private_key_hex)
108        .map_err(|e| anyhow!("Invalid private key: {}", e))?;
109    let wallet = EthereumWallet::from(signer);
110    let provider = ProviderBuilder::new()
111        .wallet(wallet)
112        .connect_provider(get_eth_provider(url)?);
113    Ok(Arc::new(provider))
114}
115
116pub async fn get_eth_ws_provider(url: &str) -> anyhow::Result<EthWsProvider> {
117    let url = Url::parse(url).map_err(|e| anyhow!("Invalid WebSocket URL: {}", e))?;
118    let ws = WsConnect::new(url);
119    let provider = ProviderBuilder::new().connect_ws(ws).await?;
120    Ok(Arc::new(provider))
121}
122
123/// Generate Bridge Authority key (Secp256k1KeyPair) and write to a file as base64 encoded `privkey`.
124pub fn generate_bridge_authority_key_and_write_to_file(
125    path: &PathBuf,
126) -> Result<(), anyhow::Error> {
127    let (_, kp): (_, BridgeAuthorityKeyPair) = get_key_pair();
128    let eth_address = BridgeAuthorityPublicKeyBytes::from(&kp.public).to_eth_address();
129    println!(
130        "Corresponding Ethereum address by this ecdsa key: {:?}",
131        eth_address
132    );
133    let sui_address = SuiAddress::from(&kp.public);
134    println!(
135        "Corresponding Sui address by this ecdsa key: {:?}",
136        sui_address
137    );
138    let base64_encoded = kp.encode_base64();
139    std::fs::write(path, base64_encoded)
140        .map_err(|err| anyhow!("Failed to write encoded key to path: {:?}", err))
141}
142
143/// Generate Bridge Client key (Secp256k1KeyPair or Ed25519KeyPair) and write to a file as base64 encoded `flag || privkey`.
144pub fn generate_bridge_client_key_and_write_to_file(
145    path: &PathBuf,
146    use_ecdsa: bool,
147) -> Result<(), anyhow::Error> {
148    let kp = if use_ecdsa {
149        let (_, kp): (_, Secp256k1KeyPair) = get_key_pair();
150        let eth_address = BridgeAuthorityPublicKeyBytes::from(&kp.public).to_eth_address();
151        println!(
152            "Corresponding Ethereum address by this ecdsa key: {:?}",
153            eth_address
154        );
155        SuiKeyPair::from(kp)
156    } else {
157        let (_, kp): (_, Ed25519KeyPair) = get_key_pair();
158        SuiKeyPair::from(kp)
159    };
160    let sui_address = SuiAddress::from(&kp.public());
161    println!("Corresponding Sui address by this key: {:?}", sui_address);
162
163    let contents = kp.encode_base64();
164    std::fs::write(path, contents)
165        .map_err(|err| anyhow!("Failed to write encoded key to path: {:?}", err))
166}
167
168/// Given the address of SuiBridge Proxy, return the addresses of the committee, limiter, vault, and config.
169pub async fn get_eth_contract_addresses(
170    bridge_proxy_address: EthAddress,
171    provider: EthProvider,
172) -> anyhow::Result<(
173    EthAddress,
174    EthAddress,
175    EthAddress,
176    EthAddress,
177    EthAddress,
178    EthAddress,
179    EthAddress,
180    EthAddress,
181)> {
182    let sui_bridge = EthSuiBridge::new(bridge_proxy_address, provider.clone());
183    let committee_address: EthAddress = sui_bridge.committee().call().await?;
184    let committee = EthBridgeCommittee::new(committee_address, provider.clone());
185    let config_address: EthAddress = committee.config().call().await?;
186    let bridge_config = EthBridgeConfig::new(config_address, provider.clone());
187    let limiter_address: EthAddress = sui_bridge.limiter().call().await?;
188    let vault_address: EthAddress = sui_bridge.vault().call().await?;
189    let vault = EthBridgeVault::new(vault_address, provider.clone());
190    let weth_address: EthAddress = vault.wETH().call().await?;
191    let usdt_address: EthAddress = bridge_config.tokenAddressOf(4).call().await?;
192    let wbtc_address: EthAddress = bridge_config.tokenAddressOf(1).call().await?;
193    let lbtc_address: EthAddress = bridge_config.tokenAddressOf(6).call().await?;
194
195    Ok((
196        committee_address,
197        limiter_address,
198        vault_address,
199        config_address,
200        weth_address,
201        usdt_address,
202        wbtc_address,
203        lbtc_address,
204    ))
205}
206
207/// Given the address of SuiBridge Proxy, return the contracts of the committee, limiter, vault, and config.
208pub async fn get_eth_contracts(
209    bridge_proxy_address: EthAddress,
210    provider: EthProvider,
211) -> anyhow::Result<EthBridgeContracts> {
212    let sui_bridge = EthSuiBridge::new(bridge_proxy_address, provider.clone());
213    let committee_address: EthAddress = sui_bridge.committee().call().await?;
214    let limiter_address: EthAddress = sui_bridge.limiter().call().await?;
215    let vault_address: EthAddress = sui_bridge.vault().call().await?;
216    let committee = EthBridgeCommittee::new(committee_address, provider.clone());
217    let config_address: EthAddress = committee.config().call().await?;
218
219    let limiter = EthBridgeLimiter::new(limiter_address, provider.clone());
220    let vault = EthBridgeVault::new(vault_address, provider.clone());
221    let config = EthBridgeConfig::new(config_address, provider.clone());
222    Ok(EthBridgeContracts {
223        bridge: sui_bridge,
224        committee,
225        limiter,
226        vault,
227        config,
228    })
229}
230
231/// Read bridge key from a file and print the corresponding information.
232/// If `is_validator_key` is true, the key must be a Secp256k1 key.
233pub fn examine_key(path: &PathBuf, is_validator_key: bool) -> Result<(), anyhow::Error> {
234    let key = read_key(path, is_validator_key)?;
235    let sui_address = SuiAddress::from(&key.public());
236    let pubkey = match key {
237        SuiKeyPair::Secp256k1(kp) => {
238            println!("Secp256k1 key:");
239            let eth_address = BridgeAuthorityPublicKeyBytes::from(&kp.public).to_eth_address();
240            println!("Corresponding Ethereum address: {:x}", eth_address);
241            kp.public.as_bytes().to_vec()
242        }
243        SuiKeyPair::Ed25519(kp) => {
244            println!("Ed25519 key:");
245            kp.public().as_bytes().to_vec()
246        }
247        SuiKeyPair::Secp256r1(kp) => {
248            println!("Secp256r1 key:");
249            kp.public().as_bytes().to_vec()
250        }
251    };
252    println!("Corresponding Sui address: {:?}", sui_address);
253    println!("Corresponding PublicKey: {:?}", Hex::encode(pubkey));
254    Ok(())
255}
256
257/// Generate Bridge Node Config template and write to a file.
258pub fn generate_bridge_node_config_and_write_to_file(
259    path: &PathBuf,
260    run_client: bool,
261) -> Result<(), anyhow::Error> {
262    let mut config = BridgeNodeConfig {
263        server_listen_port: 9191,
264        metrics_port: 9184,
265        bridge_authority_key_path: PathBuf::from("/path/to/your/bridge_authority_key"),
266        sui: SuiConfig {
267            sui_rpc_url: "your_sui_rpc_url".to_string(),
268            sui_bridge_chain_id: BridgeChainId::SuiTestnet as u8,
269            bridge_client_key_path: None,
270            bridge_client_gas_object: None,
271            sui_bridge_module_last_processed_event_id_override: None,
272            sui_bridge_next_sequence_number_override: None,
273        },
274        eth: EthConfig {
275            eth_rpc_url: "your_eth_rpc_url".to_string(),
276            eth_bridge_proxy_address: "0x0000000000000000000000000000000000000000".to_string(),
277            eth_bridge_chain_id: BridgeChainId::EthSepolia as u8,
278            eth_contracts_start_block_fallback: Some(0),
279            eth_contracts_start_block_override: None,
280        },
281        approved_governance_actions: vec![],
282        run_client,
283        db_path: None,
284        metrics_key_pair: default_ed25519_key_pair(),
285        metrics: Some(MetricsConfig {
286            push_interval_seconds: None, // use default value
287            push_url: "metrics_proxy_url".to_string(),
288        }),
289        watchdog_config: Some(WatchdogConfig {
290            total_supplies: BTreeMap::from_iter(vec![(
291                "eth".to_string(),
292                "0xd0e89b2af5e4910726fbcd8b8dd37bb79b29e5f83f7491bca830e94f7f226d29::eth::ETH"
293                    .to_string(),
294            )]),
295        }),
296    };
297    if run_client {
298        config.sui.bridge_client_key_path = Some(PathBuf::from("/path/to/your/bridge_client_key"));
299        config.db_path = Some(PathBuf::from("/path/to/your/client_db"));
300    }
301    config.save(path)
302}
303
304pub async fn publish_and_register_coins_return_add_coins_on_sui_action(
305    wallet_context: &WalletContext,
306    bridge_arg: ObjectArg,
307    token_packages_dir: Vec<PathBuf>,
308    token_ids: Vec<u8>,
309    token_prices: Vec<u64>,
310    nonce: u64,
311) -> BridgeAction {
312    assert!(token_ids.len() == token_packages_dir.len());
313    assert!(token_prices.len() == token_packages_dir.len());
314    let sui_client = wallet_context.get_client().await.unwrap();
315    let quorum_driver_api = Arc::new(sui_client.quorum_driver_api().clone());
316    let rgp = sui_client
317        .governance_api()
318        .get_reference_gas_price()
319        .await
320        .unwrap();
321
322    let senders = wallet_context.get_addresses();
323    // We want each sender to deal with one coin
324    assert!(senders.len() >= token_packages_dir.len());
325
326    // publish coin packages
327    let mut publish_tokens_tasks = vec![];
328
329    for (token_package_dir, sender) in token_packages_dir.iter().zip(senders.clone()) {
330        let gas = wallet_context
331            .get_one_gas_object_owned_by_address(sender)
332            .await
333            .unwrap()
334            .unwrap();
335        let tx = TestTransactionBuilder::new(sender, gas, rgp)
336            .publish(token_package_dir.to_path_buf())
337            .build();
338        let tx = wallet_context.sign_transaction(&tx).await;
339        let api_clone = quorum_driver_api.clone();
340        publish_tokens_tasks.push(tokio::spawn(async move {
341            api_clone.execute_transaction_block(
342                tx,
343                SuiTransactionBlockResponseOptions::new()
344                    .with_effects()
345                    .with_input()
346                    .with_events()
347                    .with_object_changes()
348                    .with_balance_changes(),
349                Some(sui_types::transaction_driver_types::ExecuteTransactionRequestType::WaitForLocalExecution),
350            ).await
351        }));
352    }
353    let publish_coin_responses = join_all(publish_tokens_tasks).await;
354
355    let mut token_type_names = vec![];
356    let mut register_tasks = vec![];
357    for (response, sender) in publish_coin_responses.into_iter().zip(senders.clone()) {
358        let response = response.unwrap().unwrap();
359        assert_eq!(
360            response.effects.unwrap().status(),
361            &SuiExecutionStatus::Success
362        );
363        let object_changes = response.object_changes.unwrap();
364        let mut tc = None;
365        let mut type_ = None;
366        let mut uc = None;
367        let mut metadata = None;
368        for object_change in &object_changes {
369            if let o @ sui_json_rpc_types::ObjectChange::Created { object_type, .. } = object_change
370            {
371                if object_type.name.as_str().starts_with("TreasuryCap") {
372                    assert!(tc.is_none() && type_.is_none());
373                    tc = Some(o.clone());
374                    type_ = Some(object_type.type_params.first().unwrap().clone());
375                } else if object_type.name.as_str().starts_with("UpgradeCap") {
376                    assert!(uc.is_none());
377                    uc = Some(o.clone());
378                } else if object_type.name.as_str().starts_with("CoinMetadata") {
379                    assert!(metadata.is_none());
380                    metadata = Some(o.clone());
381                }
382            }
383        }
384        let (tc, type_, uc, metadata) =
385            (tc.unwrap(), type_.unwrap(), uc.unwrap(), metadata.unwrap());
386
387        // register with the bridge
388        let mut builder = ProgrammableTransactionBuilder::new();
389        let bridge_arg = builder.obj(bridge_arg).unwrap();
390        let uc_arg = builder
391            .obj(ObjectArg::ImmOrOwnedObject(uc.object_ref()))
392            .unwrap();
393        let tc_arg = builder
394            .obj(ObjectArg::ImmOrOwnedObject(tc.object_ref()))
395            .unwrap();
396        let metadata_arg = builder
397            .obj(ObjectArg::ImmOrOwnedObject(metadata.object_ref()))
398            .unwrap();
399        builder.programmable_move_call(
400            BRIDGE_PACKAGE_ID,
401            BRIDGE_MODULE_NAME.into(),
402            BRIDGE_REGISTER_FOREIGN_TOKEN_FUNCTION_NAME.into(),
403            vec![type_.clone()],
404            vec![bridge_arg, tc_arg, uc_arg, metadata_arg],
405        );
406        let pt = builder.finish();
407        let gas = wallet_context
408            .get_one_gas_object_owned_by_address(sender)
409            .await
410            .unwrap()
411            .unwrap();
412        let tx = TransactionData::new_programmable(sender, vec![gas], pt, 1_000_000_000, rgp);
413        let signed_tx = wallet_context.sign_transaction(&tx).await;
414        let api_clone = quorum_driver_api.clone();
415        register_tasks.push(async move {
416            api_clone
417                .execute_transaction_block(
418                    signed_tx,
419                    SuiTransactionBlockResponseOptions::new().with_effects(),
420                    None,
421                )
422                .await
423        });
424        token_type_names.push(type_);
425    }
426    for response in join_all(register_tasks).await {
427        assert_eq!(
428            response.unwrap().effects.unwrap().status(),
429            &SuiExecutionStatus::Success
430        );
431    }
432
433    BridgeAction::AddTokensOnSuiAction(AddTokensOnSuiAction {
434        nonce,
435        chain_id: BridgeChainId::SuiCustom,
436        native: false,
437        token_ids,
438        token_type_names,
439        token_prices,
440    })
441}
442
443pub async fn wait_for_server_to_be_up(server_url: String, timeout_sec: u64) -> anyhow::Result<()> {
444    let now = std::time::Instant::now();
445    loop {
446        if let Ok(true) = reqwest::Client::new()
447            .get(server_url.clone())
448            .header(reqwest::header::ACCEPT, APPLICATION_JSON)
449            .send()
450            .await
451            .map(|res| res.status().is_success())
452        {
453            break;
454        }
455        if now.elapsed().as_secs() > timeout_sec {
456            anyhow::bail!("Server is not up and running after {} seconds", timeout_sec);
457        }
458        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
459    }
460    Ok(())
461}
462
463/// Return a mappping from validator name to their bridge voting power.
464/// If a validator is not in the Sui committee, we will use its base URL as the name.
465pub async fn get_committee_voting_power_by_name(
466    bridge_committee: &Arc<BridgeCommittee>,
467    system_state: &SuiSystemStateSummary,
468) -> BTreeMap<String, StakeUnit> {
469    let mut sui_committee: BTreeMap<_, _> = system_state
470        .active_validators
471        .iter()
472        .map(|v| (v.sui_address, v.name.clone()))
473        .collect();
474    bridge_committee
475        .members()
476        .iter()
477        .map(|v| {
478            (
479                sui_committee
480                    .remove(&v.1.sui_address)
481                    .unwrap_or(v.1.base_url.clone()),
482                v.1.voting_power,
483            )
484        })
485        .collect()
486}
487
488/// Return a mappping from validator pub keys to their names.
489/// If a validator is not in the Sui committee, we will use its base URL as the name.
490pub async fn get_validator_names_by_pub_keys(
491    bridge_committee: &Arc<BridgeCommittee>,
492    system_state: &SuiSystemStateSummary,
493) -> BTreeMap<BridgeAuthorityPublicKeyBytes, String> {
494    let mut sui_committee: BTreeMap<_, _> = system_state
495        .active_validators
496        .iter()
497        .map(|v| (v.sui_address, v.name.clone()))
498        .collect();
499    bridge_committee
500        .members()
501        .iter()
502        .map(|(name, validator)| {
503            (
504                name.clone(),
505                sui_committee
506                    .remove(&validator.sui_address)
507                    .unwrap_or(validator.base_url.clone()),
508            )
509        })
510        .collect()
511}