1use 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
122pub 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
142pub 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
167pub 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 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
249pub 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
273pub 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 };
294 println!("Corresponding Sui address: {:?}", sui_address);
295 println!("Corresponding PublicKey: {:?}", Hex::encode(pubkey));
296 Ok(())
297}
298
299pub fn generate_bridge_node_config_and_write_to_file(
301 path: &PathBuf,
302 run_client: bool,
303) -> Result<(), anyhow::Error> {
304 let mut config = BridgeNodeConfig {
305 server_listen_port: 9191,
306 metrics_port: 9184,
307 bridge_authority_key_path: PathBuf::from("/path/to/your/bridge_authority_key"),
308 sui: SuiConfig {
309 sui_rpc_url: "your_sui_rpc_url".to_string(),
310 sui_bridge_chain_id: BridgeChainId::SuiTestnet as u8,
311 bridge_client_key_path: None,
312 bridge_client_gas_object: None,
313 sui_bridge_module_last_processed_event_id_override: None,
314 sui_bridge_next_sequence_number_override: None,
315 },
316 eth: EthConfig {
317 eth_rpc_url: None, eth_rpc_urls: Some(vec!["your_eth_rpc_url".to_string()]),
319 eth_rpc_quorum: 1,
320 eth_health_check_interval_secs: 300,
321 eth_bridge_proxy_address: "0x0000000000000000000000000000000000000000".to_string(),
322 eth_bridge_chain_id: BridgeChainId::EthSepolia as u8,
323 eth_contracts_start_block_fallback: Some(0),
324 eth_contracts_start_block_override: None,
325 },
326 approved_governance_actions: vec![],
327 run_client,
328 db_path: None,
329 metrics_key_pair: default_ed25519_key_pair(),
330 metrics: Some(MetricsConfig {
331 push_interval_seconds: None, push_url: "metrics_proxy_url".to_string(),
333 }),
334 watchdog_config: Some(WatchdogConfig {
335 total_supplies: BTreeMap::from_iter(vec![(
336 "eth".to_string(),
337 "0xd0e89b2af5e4910726fbcd8b8dd37bb79b29e5f83f7491bca830e94f7f226d29::eth::ETH"
338 .to_string(),
339 )]),
340 }),
341 };
342 if run_client {
343 config.sui.bridge_client_key_path = Some(PathBuf::from("/path/to/your/bridge_client_key"));
344 config.db_path = Some(PathBuf::from("/path/to/your/client_db"));
345 }
346 config.save(path)
347}
348
349pub async fn publish_and_register_coins_return_add_coins_on_sui_action(
350 wallet_context: &WalletContext,
351 bridge_arg: ObjectArg,
352 token_packages_dir: Vec<PathBuf>,
353 token_ids: Vec<u8>,
354 token_prices: Vec<u64>,
355 nonce: u64,
356) -> BridgeAction {
357 assert!(token_ids.len() == token_packages_dir.len());
358 assert!(token_prices.len() == token_packages_dir.len());
359 let client = wallet_context.grpc_client().unwrap();
360 let rgp = client.get_reference_gas_price().await.unwrap();
361
362 let senders = wallet_context.get_addresses();
363 assert!(senders.len() >= token_packages_dir.len());
365
366 let mut publish_tokens_tasks = vec![];
368
369 for (token_package_dir, sender) in token_packages_dir.iter().zip(senders.clone()) {
370 let gas = wallet_context
371 .get_one_gas_object_owned_by_address(sender)
372 .await
373 .unwrap()
374 .unwrap();
375 let tx = TestTransactionBuilder::new(sender, gas, rgp)
376 .publish(token_package_dir.to_path_buf())
377 .build();
378 let tx = wallet_context.sign_transaction(&tx).await;
379 let api_clone = client.clone();
380 publish_tokens_tasks.push(tokio::spawn(async move {
381 api_clone
382 .execute_transaction_and_wait_for_checkpoint(&tx)
383 .await
384 }));
385 }
386 let publish_coin_responses = join_all(publish_tokens_tasks).await;
387
388 let mut token_type_names = vec![];
389 let mut register_tasks = vec![];
390 for (response, sender) in publish_coin_responses.into_iter().zip(senders.clone()) {
391 let response = response.unwrap().unwrap();
392 assert!(response.effects.status().is_ok());
393 let mut tc = None;
394 let mut type_ = None;
395 let mut uc = None;
396 let mut metadata = None;
397 for o in &response.changed_objects {
398 use sui_rpc::proto::sui::rpc::v2::changed_object::IdOperation;
399 if matches!(o.id_operation(), IdOperation::Created) {
400 let Ok(object_type) = o.object_type().parse::<StructTag>() else {
401 continue;
402 };
403 if object_type.name.as_str().starts_with("TreasuryCap") {
404 assert!(tc.is_none() && type_.is_none());
405 tc = {
406 let id = o.object_id().parse().unwrap();
407 let version = o.output_version().into();
408 let digest = o.output_digest().parse().unwrap();
409 Some((id, version, digest))
410 };
411 type_ = Some(object_type.type_params.first().unwrap().clone());
412 } else if object_type.name.as_str().starts_with("UpgradeCap") {
413 assert!(uc.is_none());
414 uc = {
415 let id = o.object_id().parse().unwrap();
416 let version = o.output_version().into();
417 let digest = o.output_digest().parse().unwrap();
418 Some((id, version, digest))
419 };
420 } else if object_type.name.as_str().starts_with("CoinMetadata") {
421 assert!(metadata.is_none());
422 metadata = {
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 }
429 }
430 }
431 let (tc, type_, uc, metadata) =
432 (tc.unwrap(), type_.unwrap(), uc.unwrap(), metadata.unwrap());
433
434 let mut builder = ProgrammableTransactionBuilder::new();
436 let bridge_arg = builder.obj(bridge_arg).unwrap();
437 let uc_arg = builder.obj(ObjectArg::ImmOrOwnedObject(uc)).unwrap();
438 let tc_arg = builder.obj(ObjectArg::ImmOrOwnedObject(tc)).unwrap();
439 let metadata_arg = builder.obj(ObjectArg::ImmOrOwnedObject(metadata)).unwrap();
440 builder.programmable_move_call(
441 BRIDGE_PACKAGE_ID,
442 BRIDGE_MODULE_NAME.into(),
443 BRIDGE_REGISTER_FOREIGN_TOKEN_FUNCTION_NAME.into(),
444 vec![type_.clone()],
445 vec![bridge_arg, tc_arg, uc_arg, metadata_arg],
446 );
447 let pt = builder.finish();
448 let gas = wallet_context
449 .get_one_gas_object_owned_by_address(sender)
450 .await
451 .unwrap()
452 .unwrap();
453 let tx = TransactionData::new_programmable(sender, vec![gas], pt, 1_000_000_000, rgp);
454 let signed_tx = wallet_context.sign_transaction(&tx).await;
455 let api_clone = client.clone();
456 register_tasks.push(async move {
457 api_clone
458 .execute_transaction_and_wait_for_checkpoint(&signed_tx)
459 .await
460 });
461 token_type_names.push(type_);
462 }
463 for response in join_all(register_tasks).await {
464 assert!(response.unwrap().effects.status().is_ok());
465 }
466
467 BridgeAction::AddTokensOnSuiAction(AddTokensOnSuiAction {
468 nonce,
469 chain_id: BridgeChainId::SuiCustom,
470 native: false,
471 token_ids,
472 token_type_names,
473 token_prices,
474 })
475}
476
477pub async fn wait_for_server_to_be_up(server_url: String, timeout_sec: u64) -> anyhow::Result<()> {
478 let now = std::time::Instant::now();
479 loop {
480 if let Ok(true) = reqwest::Client::new()
481 .get(server_url.clone())
482 .header(reqwest::header::ACCEPT, APPLICATION_JSON)
483 .send()
484 .await
485 .map(|res| res.status().is_success())
486 {
487 break;
488 }
489 if now.elapsed().as_secs() > timeout_sec {
490 anyhow::bail!("Server is not up and running after {} seconds", timeout_sec);
491 }
492 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
493 }
494 Ok(())
495}
496
497pub async fn get_committee_voting_power_by_name(
500 bridge_committee: &Arc<BridgeCommittee>,
501 system_state: &SuiSystemStateSummary,
502) -> BTreeMap<String, StakeUnit> {
503 let mut sui_committee: BTreeMap<_, _> = system_state
504 .active_validators
505 .iter()
506 .map(|v| (v.sui_address, v.name.clone()))
507 .collect();
508 bridge_committee
509 .members()
510 .iter()
511 .map(|v| {
512 (
513 sui_committee
514 .remove(&v.1.sui_address)
515 .unwrap_or(v.1.base_url.clone()),
516 v.1.voting_power,
517 )
518 })
519 .collect()
520}
521
522pub async fn get_validator_names_by_pub_keys(
525 bridge_committee: &Arc<BridgeCommittee>,
526 system_state: &SuiSystemStateSummary,
527) -> BTreeMap<BridgeAuthorityPublicKeyBytes, String> {
528 let mut sui_committee: BTreeMap<_, _> = system_state
529 .active_validators
530 .iter()
531 .map(|v| (v.sui_address, v.name.clone()))
532 .collect();
533 bridge_committee
534 .members()
535 .iter()
536 .map(|(name, validator)| {
537 (
538 name.clone(),
539 sui_committee
540 .remove(&validator.sui_address)
541 .unwrap_or(validator.base_url.clone()),
542 )
543 })
544 .collect()
545}