1use crate::abi::EthBridgeConfig;
5use crate::crypto::BridgeAuthorityKeyPair;
6use crate::error::BridgeError;
7use crate::eth_client::EthClient;
8use crate::metered_eth_provider::new_metered_eth_provider;
9use crate::metrics::BridgeMetrics;
10use crate::sui_client::SuiBridgeClient;
11use crate::types::{BridgeAction, is_route_valid};
12use crate::utils::get_eth_contract_addresses;
13use alloy::primitives::Address as EthAddress;
14use alloy::providers::Provider;
15use anyhow::anyhow;
16use futures::StreamExt;
17use serde::{Deserialize, Serialize};
18use serde_with::serde_as;
19use std::collections::BTreeMap;
20use std::collections::HashSet;
21use std::path::PathBuf;
22use std::str::FromStr;
23use std::sync::Arc;
24use sui_config::Config;
25use sui_json_rpc_types::Coin;
26use sui_keys::keypair_file::read_key;
27use sui_sdk::SuiClientBuilder;
28use sui_sdk::apis::CoinReadApi;
29use sui_types::base_types::ObjectRef;
30use sui_types::base_types::{ObjectID, SuiAddress};
31use sui_types::bridge::BridgeChainId;
32use sui_types::crypto::KeypairTraits;
33use sui_types::crypto::{NetworkKeyPair, SuiKeyPair, get_key_pair_from_rng};
34use sui_types::digests::{get_mainnet_chain_identifier, get_testnet_chain_identifier};
35use sui_types::event::EventID;
36use sui_types::object::Owner;
37use tracing::info;
38
39#[serde_as]
40#[derive(Clone, Debug, Deserialize, Serialize)]
41#[serde(rename_all = "kebab-case")]
42pub struct EthConfig {
43 pub eth_rpc_url: String,
45 pub eth_bridge_proxy_address: String,
47 pub eth_bridge_chain_id: u8,
49 pub eth_contracts_start_block_fallback: Option<u64>,
57 #[serde(skip_serializing_if = "Option::is_none")]
63 pub eth_contracts_start_block_override: Option<u64>,
64}
65
66#[serde_as]
67#[derive(Clone, Debug, Deserialize, Serialize)]
68#[serde(rename_all = "kebab-case")]
69pub struct SuiConfig {
70 pub sui_rpc_url: String,
72 pub sui_bridge_chain_id: u8,
74 #[serde(skip_serializing_if = "Option::is_none")]
77 pub bridge_client_key_path: Option<PathBuf>,
78 #[serde(skip_serializing_if = "Option::is_none")]
83 pub bridge_client_gas_object: Option<ObjectID>,
84 #[serde(skip_serializing_if = "Option::is_none")]
92 pub sui_bridge_module_last_processed_event_id_override: Option<EventID>,
93 #[serde(skip_serializing_if = "Option::is_none")]
97 pub sui_bridge_next_sequence_number_override: Option<u64>,
98}
99
100#[serde_as]
101#[derive(Debug, Deserialize, Serialize)]
102#[serde(rename_all = "kebab-case")]
103pub struct BridgeNodeConfig {
104 pub server_listen_port: u16,
106 pub metrics_port: u16,
108 pub bridge_authority_key_path: PathBuf,
110 pub run_client: bool,
113 #[serde(skip_serializing_if = "Option::is_none")]
115 pub db_path: Option<PathBuf>,
116 pub approved_governance_actions: Vec<BridgeAction>,
118 pub sui: SuiConfig,
120 pub eth: EthConfig,
122 #[serde(default = "default_ed25519_key_pair")]
124 pub metrics_key_pair: NetworkKeyPair,
125 #[serde(skip_serializing_if = "Option::is_none")]
126 pub metrics: Option<MetricsConfig>,
127
128 #[serde(skip_serializing_if = "Option::is_none")]
129 pub watchdog_config: Option<WatchdogConfig>,
130}
131
132pub fn default_ed25519_key_pair() -> NetworkKeyPair {
133 get_key_pair_from_rng(&mut rand::rngs::OsRng).1
134}
135
136#[derive(Debug, Clone, Deserialize, Serialize)]
137#[serde(rename_all = "kebab-case")]
138pub struct MetricsConfig {
139 #[serde(skip_serializing_if = "Option::is_none")]
140 pub push_interval_seconds: Option<u64>,
141 pub push_url: String,
142}
143
144#[derive(Debug, Clone, Deserialize, Serialize)]
145#[serde(rename_all = "kebab-case")]
146pub struct WatchdogConfig {
147 pub total_supplies: BTreeMap<String, String>,
149}
150
151impl Config for BridgeNodeConfig {}
152
153impl BridgeNodeConfig {
154 pub async fn validate(
155 &self,
156 metrics: Arc<BridgeMetrics>,
157 ) -> anyhow::Result<(BridgeServerConfig, Option<BridgeClientConfig>)> {
158 info!("Starting config validation");
159 if !is_route_valid(
160 BridgeChainId::try_from(self.sui.sui_bridge_chain_id)?,
161 BridgeChainId::try_from(self.eth.eth_bridge_chain_id)?,
162 ) {
163 return Err(anyhow!(
164 "Route between Sui chain id {} and Eth chain id {} is not valid",
165 self.sui.sui_bridge_chain_id,
166 self.eth.eth_bridge_chain_id,
167 ));
168 };
169
170 let bridge_authority_key = match read_key(&self.bridge_authority_key_path, true)? {
171 SuiKeyPair::Secp256k1(key) => key,
172 _ => unreachable!("we required secp256k1 key in `read_key`"),
173 };
174
175 let sui_client =
178 Arc::new(SuiBridgeClient::new(&self.sui.sui_rpc_url, metrics.clone()).await?);
179 let bridge_committee = sui_client
180 .get_bridge_committee()
181 .await
182 .map_err(|e| anyhow!("Error getting bridge committee: {:?}", e))?;
183 if !bridge_committee.is_active_member(&bridge_authority_key.public().into()) {
184 return Err(anyhow!(
185 "Bridge authority key is not part of bridge committee"
186 ));
187 }
188
189 let (eth_client, eth_contracts) = self.prepare_for_eth(metrics.clone()).await?;
190 let bridge_summary = sui_client
191 .get_bridge_summary()
192 .await
193 .map_err(|e| anyhow!("Error getting bridge summary: {:?}", e))?;
194 if bridge_summary.chain_id != self.sui.sui_bridge_chain_id {
195 anyhow::bail!(
196 "Bridge chain id mismatch: expected {}, but connected to {}",
197 self.sui.sui_bridge_chain_id,
198 bridge_summary.chain_id
199 );
200 }
201
202 for action in &self.approved_governance_actions {
204 if !action.is_governance_action() {
205 anyhow::bail!(format!(
206 "{:?}",
207 BridgeError::ActionIsNotGovernanceAction(Box::new(action.clone()))
208 ));
209 }
210 }
211 let approved_governance_actions = self.approved_governance_actions.clone();
212
213 let bridge_server_config = BridgeServerConfig {
214 key: bridge_authority_key,
215 metrics_port: self.metrics_port,
216 eth_bridge_proxy_address: eth_contracts[0], server_listen_port: self.server_listen_port,
218 sui_client: sui_client.clone(),
219 eth_client: eth_client.clone(),
220 approved_governance_actions,
221 };
222 if !self.run_client {
223 return Ok((bridge_server_config, None));
224 }
225
226 let (bridge_client_key, client_sui_address, gas_object_ref) =
228 self.prepare_for_sui(sui_client.clone(), metrics).await?;
229
230 let db_path = self
231 .db_path
232 .clone()
233 .ok_or(anyhow!("`db_path` is required when `run_client` is true"))?;
234
235 let bridge_client_config = BridgeClientConfig {
236 sui_address: client_sui_address,
237 key: bridge_client_key,
238 gas_object_ref,
239 metrics_port: self.metrics_port,
240 sui_client: sui_client.clone(),
241 eth_client: eth_client.clone(),
242 db_path,
243 eth_contracts,
244 eth_contracts_start_block_fallback: self
246 .eth
247 .eth_contracts_start_block_fallback
248 .unwrap(),
249 eth_contracts_start_block_override: self.eth.eth_contracts_start_block_override,
250 sui_bridge_module_last_processed_event_id_override: self
251 .sui
252 .sui_bridge_module_last_processed_event_id_override,
253 sui_bridge_next_sequence_number_override: self
254 .sui
255 .sui_bridge_next_sequence_number_override,
256 sui_bridge_chain_id: self.sui.sui_bridge_chain_id,
257 };
258
259 info!("Config validation complete");
260 Ok((bridge_server_config, Some(bridge_client_config)))
261 }
262
263 async fn prepare_for_eth(
264 &self,
265 metrics: Arc<BridgeMetrics>,
266 ) -> anyhow::Result<(Arc<EthClient>, Vec<EthAddress>)> {
267 info!("Creating Ethereum client provider");
268 let bridge_proxy_address = EthAddress::from_str(&self.eth.eth_bridge_proxy_address)?;
269 let provider = new_metered_eth_provider(&self.eth.eth_rpc_url, metrics.clone()).unwrap();
270 let chain_id = provider.get_chain_id().await?;
271 let (
272 committee_address,
273 limiter_address,
274 vault_address,
275 config_address,
276 _weth_address,
277 _usdt_address,
278 _wbtc_address,
279 _lbtc_address,
280 ) = get_eth_contract_addresses(bridge_proxy_address, provider.clone()).await?;
281 let config = EthBridgeConfig::new(config_address, provider.clone());
282
283 if self.run_client && self.eth.eth_contracts_start_block_fallback.is_none() {
284 return Err(anyhow!(
285 "eth_contracts_start_block_fallback is required when run_client is true"
286 ));
287 }
288
289 let bridge_chain_id: u8 = config.chainID().call().await?;
292 if self.eth.eth_bridge_chain_id != bridge_chain_id {
293 return Err(anyhow!(
294 "Bridge chain id mismatch: expected {}, but connected to {}",
295 self.eth.eth_bridge_chain_id,
296 bridge_chain_id
297 ));
298 }
299 if bridge_chain_id == BridgeChainId::EthMainnet as u8 && chain_id != 1 {
300 anyhow::bail!("Expected Eth chain id 1, but connected to {}", chain_id);
301 }
302 if bridge_chain_id == BridgeChainId::EthSepolia as u8 && chain_id != 11155111 {
303 anyhow::bail!(
304 "Expected Eth chain id 11155111, but connected to {}",
305 chain_id
306 );
307 }
308 info!(
309 "Connected to Eth chain: {}, Bridge chain id: {}",
310 chain_id, bridge_chain_id,
311 );
312
313 let eth_client = Arc::new(
314 EthClient::new(
315 &self.eth.eth_rpc_url,
316 HashSet::from_iter(vec![
317 bridge_proxy_address,
318 committee_address,
319 config_address,
320 limiter_address,
321 vault_address,
322 ]),
323 metrics,
324 )
325 .await?,
326 );
327 let contract_addresses = vec![
328 bridge_proxy_address,
329 committee_address,
330 config_address,
331 limiter_address,
332 vault_address,
333 ];
334 info!("Ethereum client setup complete");
335 Ok((eth_client, contract_addresses))
336 }
337
338 async fn prepare_for_sui(
339 &self,
340 sui_client: Arc<SuiBridgeClient>,
341 metrics: Arc<BridgeMetrics>,
342 ) -> anyhow::Result<(SuiKeyPair, SuiAddress, ObjectRef)> {
343 let bridge_client_key = match &self.sui.bridge_client_key_path {
344 None => read_key(&self.bridge_authority_key_path, true),
345 Some(path) => read_key(path, false),
346 }?;
347
348 let sui_identifier = sui_client
351 .get_chain_identifier()
352 .await
353 .map_err(|e| anyhow!("Error getting chain identifier from Sui: {:?}", e))?;
354 if self.sui.sui_bridge_chain_id == BridgeChainId::SuiMainnet as u8
355 && sui_identifier != get_mainnet_chain_identifier().to_string()
356 {
357 anyhow::bail!(
358 "Expected sui chain identifier {}, but connected to {}",
359 self.sui.sui_bridge_chain_id,
360 sui_identifier
361 );
362 }
363 if self.sui.sui_bridge_chain_id == BridgeChainId::SuiTestnet as u8
364 && sui_identifier != get_testnet_chain_identifier().to_string()
365 {
366 anyhow::bail!(
367 "Expected sui chain identifier {}, but connected to {}",
368 self.sui.sui_bridge_chain_id,
369 sui_identifier
370 );
371 }
372 info!(
373 "Connected to Sui chain: {}, Bridge chain id: {}",
374 sui_identifier, self.sui.sui_bridge_chain_id,
375 );
376
377 let client_sui_address = SuiAddress::from(&bridge_client_key.public());
378
379 let gas_object_id = match self.sui.bridge_client_gas_object {
380 Some(id) => id,
381 None => {
382 info!("No gas object configured, finding gas object with highest balance");
383 let sui_client = SuiClientBuilder::default()
384 .build(&self.sui.sui_rpc_url)
385 .await?;
386 let coin =
387 pick_highest_balance_coin(sui_client.coin_read_api(), client_sui_address, 10_000_000_000)
389 .await?;
390 coin.coin_object_id
391 }
392 };
393 let (gas_coin, gas_object_ref, owner) = sui_client
394 .get_gas_data_panic_if_not_gas(gas_object_id)
395 .await;
396 if owner != Owner::AddressOwner(client_sui_address) {
397 return Err(anyhow!(
398 "Gas object {:?} is not owned by bridge client key's associated sui address {:?}, but {:?}",
399 gas_object_id,
400 client_sui_address,
401 owner
402 ));
403 }
404 let balance = gas_coin.value();
405 info!("Gas object balance: {}", balance);
406 metrics.gas_coin_balance.set(balance as i64);
407
408 info!("Sui client setup complete");
409 Ok((bridge_client_key, client_sui_address, gas_object_ref))
410 }
411}
412
413pub struct BridgeServerConfig {
414 pub key: BridgeAuthorityKeyPair,
415 pub server_listen_port: u16,
416 pub eth_bridge_proxy_address: EthAddress,
417 pub metrics_port: u16,
418 pub sui_client: Arc<SuiBridgeClient>,
419 pub eth_client: Arc<EthClient>,
420 pub approved_governance_actions: Vec<BridgeAction>,
422}
423
424pub struct BridgeClientConfig {
425 pub sui_address: SuiAddress,
426 pub key: SuiKeyPair,
427 pub gas_object_ref: ObjectRef,
428 pub metrics_port: u16,
429 pub sui_client: Arc<SuiBridgeClient>,
430 pub eth_client: Arc<EthClient>,
431 pub db_path: PathBuf,
432 pub eth_contracts: Vec<EthAddress>,
433 pub eth_contracts_start_block_fallback: u64,
435 pub eth_contracts_start_block_override: Option<u64>,
436 pub sui_bridge_module_last_processed_event_id_override: Option<EventID>,
437 pub sui_bridge_next_sequence_number_override: Option<u64>,
438 pub sui_bridge_chain_id: u8,
439}
440
441#[serde_as]
442#[derive(Clone, Debug, Deserialize, Serialize)]
443#[serde(rename_all = "kebab-case")]
444pub struct BridgeCommitteeConfig {
445 pub bridge_authority_port_and_key_path: Vec<(u64, PathBuf)>,
446}
447
448impl Config for BridgeCommitteeConfig {}
449
450pub async fn pick_highest_balance_coin(
451 coin_read_api: &CoinReadApi,
452 address: SuiAddress,
453 minimal_amount: u64,
454) -> anyhow::Result<Coin> {
455 info!("Looking for a suitable gas coin for address {:?}", address);
456
457 let mut stream = coin_read_api
459 .get_coins_stream(address, Some("0x2::sui::SUI".to_string()))
460 .boxed();
461
462 let mut coins_checked = 0;
463
464 while let Some(coin) = stream.next().await {
465 info!(
466 "Checking coin: {:?}, balance: {}",
467 coin.coin_object_id, coin.balance
468 );
469 coins_checked += 1;
470
471 if coin.balance >= minimal_amount {
473 info!(
474 "Found suitable gas coin with {} mist (object ID: {:?})",
475 coin.balance, coin.coin_object_id
476 );
477 return Ok(coin);
478 }
479
480 if coins_checked >= 1000 {
482 break;
483 }
484 }
485
486 Err(anyhow!(
487 "No suitable gas coin with >= {} mist found for address {:?} after checking {} coins",
488 minimal_amount,
489 address,
490 coins_checked
491 ))
492}
493
494#[derive(Debug, Eq, PartialEq, Clone)]
495pub struct EthContractAddresses {
496 pub sui_bridge: EthAddress,
497 pub bridge_committee: EthAddress,
498 pub bridge_config: EthAddress,
499 pub bridge_limiter: EthAddress,
500 pub bridge_vault: EthAddress,
501}