Skip to main content

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