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