sui_bridge/server/
mod.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4#![allow(clippy::inconsistent_digit_grouping)]
5use crate::crypto::BridgeAuthorityPublicKeyBytes;
6use crate::error::BridgeError;
7use crate::metrics::BridgeMetrics;
8use crate::server::handler::BridgeRequestHandlerTrait;
9use crate::types::{
10    AddTokensOnEvmAction, AddTokensOnSuiAction, AssetPriceUpdateAction, BlocklistCommitteeAction,
11    BlocklistType, BridgeAction, EmergencyAction, EmergencyActionType, EvmContractUpgradeAction,
12    LimitUpdateAction, SignedBridgeAction,
13};
14use crate::with_metrics;
15use alloy::primitives::Address as EthAddress;
16use axum::Json;
17use axum::Router;
18use axum::extract::{Path, State};
19use axum::http::StatusCode;
20use axum::routing::get;
21use fastcrypto::ed25519::Ed25519PublicKey;
22use fastcrypto::encoding::{Encoding, Hex};
23use fastcrypto::traits::ToFromBytes;
24use std::net::SocketAddr;
25use std::str::FromStr;
26use std::sync::Arc;
27use sui_types::TypeTag;
28use sui_types::bridge::BridgeChainId;
29use tracing::{info, instrument};
30
31pub mod governance_verifier;
32pub mod handler;
33
34#[cfg(any(feature = "test-utils", test))]
35pub(crate) mod mock_handler;
36
37pub const APPLICATION_JSON: &str = "application/json";
38
39// Maximum number of items allowed in comma-separated lists in governance endpoints
40// This prevents DoS attacks where oversized lists cause panics during u8 conversion
41pub const MAX_LIST_SIZE: usize = 255;
42
43pub const PING_PATH: &str = "/ping";
44pub const METRICS_KEY_PATH: &str = "/metrics_pub_key";
45
46// Important: for BridgeActions, the paths need to match the ones in bridge_client.rs
47pub const ETH_TO_SUI_TX_PATH: &str = "/sign/bridge_tx/eth/sui/{tx_hash}/{event_index}";
48pub const SUI_TO_ETH_TX_PATH: &str = "/sign/bridge_tx/sui/eth/{tx_digest}/{event_index}";
49pub const SUI_TO_ETH_TRANSFER_PATH: &str =
50    "/sign/bridge_action/sui/eth/{source_chain}/{message_type}/{bridge_seq_num}";
51pub const COMMITTEE_BLOCKLIST_UPDATE_PATH: &str =
52    "/sign/update_committee_blocklist/{chain_id}/{nonce}/{type}/{keys}";
53pub const EMERGENCY_BUTTON_PATH: &str = "/sign/emergency_button/{chain_id}/{nonce}/{type}";
54pub const LIMIT_UPDATE_PATH: &str =
55    "/sign/update_limit/{chain_id}/{nonce}/{sending_chain_id}/{new_usd_limit}";
56pub const ASSET_PRICE_UPDATE_PATH: &str =
57    "/sign/update_asset_price/{chain_id}/{nonce}/{token_id}/{new_usd_price}";
58pub const EVM_CONTRACT_UPGRADE_PATH_WITH_CALLDATA: &str =
59    "/sign/upgrade_evm_contract/{chain_id}/{nonce}/{proxy_address}/{new_impl_address}/{calldata}";
60pub const EVM_CONTRACT_UPGRADE_PATH: &str =
61    "/sign/upgrade_evm_contract/{chain_id}/{nonce}/{proxy_address}/{new_impl_address}";
62pub const ADD_TOKENS_ON_SUI_PATH: &str = "/sign/add_tokens_on_sui/{chain_id}/{nonce}/{native}/{token_ids}/{token_type_names}/{token_prices}";
63pub const ADD_TOKENS_ON_EVM_PATH: &str = "/sign/add_tokens_on_evm/{chain_id}/{nonce}/{native}/{token_ids}/{token_addresses}/{token_sui_decimals}/{token_prices}";
64
65// BridgeNode's public metadata that is accessible via the `/ping` endpoint.
66// Be careful with what to put here, as it is public.
67#[derive(serde::Serialize)]
68pub struct BridgeNodePublicMetadata {
69    pub version: &'static str,
70    pub metrics_pubkey: Option<Arc<Ed25519PublicKey>>,
71}
72
73impl BridgeNodePublicMetadata {
74    pub fn new(version: &'static str, metrics_pubkey: Ed25519PublicKey) -> Self {
75        Self {
76            version,
77            metrics_pubkey: Some(metrics_pubkey.into()),
78        }
79    }
80
81    pub fn empty_for_testing() -> Self {
82        Self {
83            version: "testing",
84            metrics_pubkey: None,
85        }
86    }
87}
88
89pub fn run_server(
90    socket_address: &SocketAddr,
91    handler: impl BridgeRequestHandlerTrait + Sync + Send + 'static,
92    metrics: Arc<BridgeMetrics>,
93    metadata: Arc<BridgeNodePublicMetadata>,
94) -> tokio::task::JoinHandle<()> {
95    let socket_address = *socket_address;
96    tokio::spawn(async move {
97        let listener = tokio::net::TcpListener::bind(socket_address).await.unwrap();
98        axum::serve(
99            listener,
100            make_router(Arc::new(handler), metrics, metadata).into_make_service(),
101        )
102        .await
103        .unwrap();
104    })
105}
106
107pub(crate) fn make_router(
108    handler: Arc<impl BridgeRequestHandlerTrait + Sync + Send + 'static>,
109    metrics: Arc<BridgeMetrics>,
110    metadata: Arc<BridgeNodePublicMetadata>,
111) -> Router {
112    Router::new()
113        .route("/", get(health_check))
114        .route(PING_PATH, get(ping))
115        .route(METRICS_KEY_PATH, get(metrics_key_fetch))
116        .route(ETH_TO_SUI_TX_PATH, get(handle_eth_tx_hash))
117        .route(SUI_TO_ETH_TX_PATH, get(handle_sui_tx_digest))
118        .route(SUI_TO_ETH_TRANSFER_PATH, get(handle_sui_token_transfer))
119        .route(
120            COMMITTEE_BLOCKLIST_UPDATE_PATH,
121            get(handle_update_committee_blocklist_action),
122        )
123        .route(EMERGENCY_BUTTON_PATH, get(handle_emergency_action))
124        .route(LIMIT_UPDATE_PATH, get(handle_limit_update_action))
125        .route(
126            ASSET_PRICE_UPDATE_PATH,
127            get(handle_asset_price_update_action),
128        )
129        .route(EVM_CONTRACT_UPGRADE_PATH, get(handle_evm_contract_upgrade))
130        .route(
131            EVM_CONTRACT_UPGRADE_PATH_WITH_CALLDATA,
132            get(handle_evm_contract_upgrade_with_calldata),
133        )
134        .route(ADD_TOKENS_ON_SUI_PATH, get(handle_add_tokens_on_sui))
135        .route(ADD_TOKENS_ON_EVM_PATH, get(handle_add_tokens_on_evm))
136        .with_state((handler, metrics, metadata))
137}
138
139impl axum::response::IntoResponse for BridgeError {
140    // TODO: distinguish client error.
141    fn into_response(self) -> axum::response::Response {
142        (
143            StatusCode::INTERNAL_SERVER_ERROR,
144            format!("Something went wrong: {:?}", self),
145        )
146            .into_response()
147    }
148}
149
150impl<E> From<E> for BridgeError
151where
152    E: Into<anyhow::Error>,
153{
154    fn from(err: E) -> Self {
155        Self::Generic(err.into().to_string())
156    }
157}
158
159async fn health_check() -> StatusCode {
160    StatusCode::OK
161}
162
163/// Validates that a comma-separated list doesn't exceed the maximum allowed size
164/// to prevent DoS attacks during u8 conversion in encoding
165fn validate_list_size(list_str: &str, field_name: &str) -> Result<(), BridgeError> {
166    let count = list_str.split(',').count();
167    if count > MAX_LIST_SIZE {
168        return Err(BridgeError::InvalidBridgeClientRequest(format!(
169            "{} list size {} exceeds maximum allowed size of {}",
170            field_name, count, MAX_LIST_SIZE
171        )));
172    }
173    Ok(())
174}
175
176async fn ping(
177    State((_handler, _metrics, metadata)): State<(
178        Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
179        Arc<BridgeMetrics>,
180        Arc<BridgeNodePublicMetadata>,
181    )>,
182) -> Result<Json<Arc<BridgeNodePublicMetadata>>, BridgeError> {
183    Ok(Json(metadata))
184}
185
186async fn metrics_key_fetch(
187    State((_handler, _metrics, metadata)): State<(
188        Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
189        Arc<BridgeMetrics>,
190        Arc<BridgeNodePublicMetadata>,
191    )>,
192) -> Result<Json<Option<Arc<Ed25519PublicKey>>>, BridgeError> {
193    Ok(Json(metadata.metrics_pubkey.clone()))
194}
195
196#[instrument(level = "error", skip_all, fields(tx_hash_hex=tx_hash_hex, event_idx=event_idx))]
197async fn handle_eth_tx_hash(
198    Path((tx_hash_hex, event_idx)): Path<(String, u16)>,
199    State((handler, metrics, _metadata)): State<(
200        Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
201        Arc<BridgeMetrics>,
202        Arc<BridgeNodePublicMetadata>,
203    )>,
204) -> Result<Json<SignedBridgeAction>, BridgeError> {
205    let future = async {
206        let sig = handler.handle_eth_tx_hash(tx_hash_hex, event_idx).await?;
207        Ok(sig)
208    };
209    with_metrics!(metrics.clone(), "handle_eth_tx_hash", future).await
210}
211
212#[instrument(level = "error", skip_all, fields(tx_digest_base58=tx_digest_base58, event_idx=event_idx))]
213async fn handle_sui_tx_digest(
214    Path((tx_digest_base58, event_idx)): Path<(String, u16)>,
215    State((handler, metrics, _metadata)): State<(
216        Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
217        Arc<BridgeMetrics>,
218        Arc<BridgeNodePublicMetadata>,
219    )>,
220) -> Result<Json<SignedBridgeAction>, BridgeError> {
221    let future = async {
222        let sig: Json<SignedBridgeAction> = handler
223            .handle_sui_tx_digest(tx_digest_base58, event_idx)
224            .await?;
225        Ok(sig)
226    };
227    with_metrics!(metrics.clone(), "handle_sui_tx_digest", future).await
228}
229
230#[instrument(level = "error", skip_all, fields(source_chain=source_chain, message_type=message_type, bridge_seq_num=bridge_seq_num))]
231async fn handle_sui_token_transfer(
232    Path((source_chain, message_type, bridge_seq_num)): Path<(u8, u8, u64)>,
233    State((handler, metrics, _metadata)): State<(
234        Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
235        Arc<BridgeMetrics>,
236        Arc<BridgeNodePublicMetadata>,
237    )>,
238) -> Result<Json<SignedBridgeAction>, BridgeError> {
239    let future = async {
240        let sig: Json<SignedBridgeAction> = handler
241            .handle_sui_token_transfer(source_chain, message_type, bridge_seq_num)
242            .await?;
243        Ok(sig)
244    };
245    with_metrics!(metrics.clone(), "handle_sui_token_transfer", future).await
246}
247
248#[instrument(level = "error", skip_all, fields(chain_id=chain_id, nonce=nonce, blocklist_type=blocklist_type, keys=keys))]
249async fn handle_update_committee_blocklist_action(
250    Path((chain_id, nonce, blocklist_type, keys)): Path<(u8, u64, u8, String)>,
251    State((handler, metrics, _metadata)): State<(
252        Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
253        Arc<BridgeMetrics>,
254        Arc<BridgeNodePublicMetadata>,
255    )>,
256) -> Result<Json<SignedBridgeAction>, BridgeError> {
257    let future = async {
258        let chain_id = BridgeChainId::try_from(chain_id).map_err(|err| {
259            BridgeError::InvalidBridgeClientRequest(format!("Invalid chain id: {:?}", err))
260        })?;
261        let blocklist_type = BlocklistType::try_from(blocklist_type).map_err(|err| {
262            BridgeError::InvalidBridgeClientRequest(format!(
263                "Invalid blocklist action type: {:?}",
264                err
265            ))
266        })?;
267        // Validate list size to prevent DoS
268        validate_list_size(&keys, "keys")?;
269        let members_to_update = keys
270            .split(',')
271            .map(|s| {
272                let bytes = Hex::decode(s).map_err(|e| anyhow::anyhow!("{:?}", e))?;
273                BridgeAuthorityPublicKeyBytes::from_bytes(&bytes)
274                    .map_err(|e| anyhow::anyhow!("{:?}", e))
275            })
276            .collect::<Result<Vec<_>, _>>()
277            .map_err(|e| BridgeError::InvalidBridgeClientRequest(format!("{:?}", e)))?;
278        let action = BridgeAction::BlocklistCommitteeAction(BlocklistCommitteeAction {
279            chain_id,
280            nonce,
281            blocklist_type,
282            members_to_update,
283        });
284
285        let sig: Json<SignedBridgeAction> = handler.handle_governance_action(action).await?;
286        Ok(sig)
287    };
288    with_metrics!(
289        metrics.clone(),
290        "handle_update_committee_blocklist_action",
291        future
292    )
293    .await
294}
295
296#[instrument(level = "error", skip_all, fields(chain_id=chain_id, nonce=nonce, action_type=action_type))]
297async fn handle_emergency_action(
298    Path((chain_id, nonce, action_type)): Path<(u8, u64, u8)>,
299    State((handler, metrics, _metadata)): State<(
300        Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
301        Arc<BridgeMetrics>,
302        Arc<BridgeNodePublicMetadata>,
303    )>,
304) -> Result<Json<SignedBridgeAction>, BridgeError> {
305    let future = async {
306        let chain_id = BridgeChainId::try_from(chain_id).map_err(|err| {
307            BridgeError::InvalidBridgeClientRequest(format!("Invalid chain id: {:?}", err))
308        })?;
309        let action_type = EmergencyActionType::try_from(action_type).map_err(|err| {
310            BridgeError::InvalidBridgeClientRequest(format!(
311                "Invalid emergency action type: {:?}",
312                err
313            ))
314        })?;
315        let action = BridgeAction::EmergencyAction(EmergencyAction {
316            chain_id,
317            nonce,
318            action_type,
319        });
320        let sig: Json<SignedBridgeAction> = handler.handle_governance_action(action).await?;
321        Ok(sig)
322    };
323    with_metrics!(metrics.clone(), "handle_emergency_action", future).await
324}
325
326#[instrument(level = "error", skip_all, fields(chain_id=chain_id, nonce=nonce, sending_chain_id=sending_chain_id, new_usd_limit=new_usd_limit))]
327async fn handle_limit_update_action(
328    Path((chain_id, nonce, sending_chain_id, new_usd_limit)): Path<(u8, u64, u8, u64)>,
329    State((handler, metrics, _metadata)): State<(
330        Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
331        Arc<BridgeMetrics>,
332        Arc<BridgeNodePublicMetadata>,
333    )>,
334) -> Result<Json<SignedBridgeAction>, BridgeError> {
335    let future = async {
336        let chain_id = BridgeChainId::try_from(chain_id).map_err(|err| {
337            BridgeError::InvalidBridgeClientRequest(format!("Invalid chain id: {:?}", err))
338        })?;
339        let sending_chain_id = BridgeChainId::try_from(sending_chain_id).map_err(|err| {
340            BridgeError::InvalidBridgeClientRequest(format!("Invalid chain id: {:?}", err))
341        })?;
342        let action = BridgeAction::LimitUpdateAction(LimitUpdateAction {
343            chain_id,
344            nonce,
345            sending_chain_id,
346            new_usd_limit,
347        });
348        let sig: Json<SignedBridgeAction> = handler.handle_governance_action(action).await?;
349        Ok(sig)
350    };
351    with_metrics!(metrics.clone(), "handle_limit_update_action", future).await
352}
353
354#[instrument(level = "error", skip_all, fields(chain_id=chain_id, nonce=nonce, token_id=token_id, new_usd_price=new_usd_price))]
355async fn handle_asset_price_update_action(
356    Path((chain_id, nonce, token_id, new_usd_price)): Path<(u8, u64, u8, u64)>,
357    State((handler, metrics, _metadata)): State<(
358        Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
359        Arc<BridgeMetrics>,
360        Arc<BridgeNodePublicMetadata>,
361    )>,
362) -> Result<Json<SignedBridgeAction>, BridgeError> {
363    let future = async {
364        let chain_id = BridgeChainId::try_from(chain_id).map_err(|err| {
365            BridgeError::InvalidBridgeClientRequest(format!("Invalid chain id: {:?}", err))
366        })?;
367        let action = BridgeAction::AssetPriceUpdateAction(AssetPriceUpdateAction {
368            chain_id,
369            nonce,
370            token_id,
371            new_usd_price,
372        });
373        let sig: Json<SignedBridgeAction> = handler.handle_governance_action(action).await?;
374        Ok(sig)
375    };
376    with_metrics!(metrics.clone(), "handle_asset_price_update_action", future).await
377}
378
379#[instrument(level = "error", skip_all, fields(chain_id=chain_id, nonce=nonce, proxy_address=format!("{:x}", proxy_address), new_impl_address=format!("{:x}", new_impl_address)))]
380async fn handle_evm_contract_upgrade_with_calldata(
381    Path((chain_id, nonce, proxy_address, new_impl_address, calldata)): Path<(
382        u8,
383        u64,
384        EthAddress,
385        EthAddress,
386        String,
387    )>,
388    State((handler, metrics, _metadata)): State<(
389        Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
390        Arc<BridgeMetrics>,
391        Arc<BridgeNodePublicMetadata>,
392    )>,
393) -> Result<Json<SignedBridgeAction>, BridgeError> {
394    let future = async {
395        let chain_id = BridgeChainId::try_from(chain_id).map_err(|err| {
396            BridgeError::InvalidBridgeClientRequest(format!("Invalid chain id: {:?}", err))
397        })?;
398        let call_data = Hex::decode(&calldata).map_err(|e| {
399            BridgeError::InvalidBridgeClientRequest(format!("Invalid call data: {:?}", e))
400        })?;
401        let action = BridgeAction::EvmContractUpgradeAction(EvmContractUpgradeAction {
402            chain_id,
403            nonce,
404            proxy_address,
405            new_impl_address,
406            call_data,
407        });
408        let sig: Json<SignedBridgeAction> = handler.handle_governance_action(action).await?;
409        Ok(sig)
410    };
411    with_metrics!(
412        metrics.clone(),
413        "handle_evm_contract_upgrade_with_calldata",
414        future
415    )
416    .await
417}
418
419#[instrument(
420    level = "error",
421    skip_all,
422    fields(chain_id, nonce, proxy_address, new_impl_address)
423)]
424async fn handle_evm_contract_upgrade(
425    Path((chain_id, nonce, proxy_address, new_impl_address)): Path<(
426        u8,
427        u64,
428        EthAddress,
429        EthAddress,
430    )>,
431    State((handler, metrics, _metadata)): State<(
432        Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
433        Arc<BridgeMetrics>,
434        Arc<BridgeNodePublicMetadata>,
435    )>,
436) -> Result<Json<SignedBridgeAction>, BridgeError> {
437    let future = async {
438        let chain_id = BridgeChainId::try_from(chain_id).map_err(|err| {
439            BridgeError::InvalidBridgeClientRequest(format!("Invalid chain id: {:?}", err))
440        })?;
441        let action = BridgeAction::EvmContractUpgradeAction(EvmContractUpgradeAction {
442            chain_id,
443            nonce,
444            proxy_address,
445            new_impl_address,
446            call_data: vec![],
447        });
448        let sig: Json<SignedBridgeAction> = handler.handle_governance_action(action).await?;
449
450        Ok(sig)
451    };
452    with_metrics!(metrics.clone(), "handle_evm_contract_upgrade", future).await
453}
454
455#[instrument(level = "error", skip_all, fields(chain_id=chain_id, nonce=nonce, native=native, token_ids=token_ids, token_type_names=token_type_names, token_prices=token_prices))]
456async fn handle_add_tokens_on_sui(
457    Path((chain_id, nonce, native, token_ids, token_type_names, token_prices)): Path<(
458        u8,
459        u64,
460        u8,
461        String,
462        String,
463        String,
464    )>,
465    State((handler, metrics, _metadata)): State<(
466        Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
467        Arc<BridgeMetrics>,
468        Arc<BridgeNodePublicMetadata>,
469    )>,
470) -> Result<Json<SignedBridgeAction>, BridgeError> {
471    let future = async {
472        let chain_id = BridgeChainId::try_from(chain_id).map_err(|err| {
473            BridgeError::InvalidBridgeClientRequest(format!("Invalid chain id: {:?}", err))
474        })?;
475
476        if !chain_id.is_sui_chain() {
477            return Err(BridgeError::InvalidBridgeClientRequest(
478                "handle_add_tokens_on_sui only expects Sui chain id".to_string(),
479            ));
480        }
481
482        let native = match native {
483            1 => true,
484            0 => false,
485            _ => {
486                return Err(BridgeError::InvalidBridgeClientRequest(format!(
487                    "Invalid native flag: {}",
488                    native
489                )));
490            }
491        };
492        // Validate list sizes to prevent DoS
493        validate_list_size(&token_ids, "token_ids")?;
494        validate_list_size(&token_type_names, "token_type_names")?;
495        validate_list_size(&token_prices, "token_prices")?;
496
497        let token_ids = token_ids
498            .split(',')
499            .map(|s| {
500                s.parse::<u8>().map_err(|err| {
501                    BridgeError::InvalidBridgeClientRequest(format!("Invalid token id: {:?}", err))
502                })
503            })
504            .collect::<Result<Vec<_>, _>>()?;
505        let token_type_names = token_type_names
506            .split(',')
507            .map(|s| {
508                TypeTag::from_str(s).map_err(|err| {
509                    BridgeError::InvalidBridgeClientRequest(format!(
510                        "Invalid token type name: {:?}",
511                        err
512                    ))
513                })
514            })
515            .collect::<Result<Vec<_>, _>>()?;
516        let token_prices = token_prices
517            .split(',')
518            .map(|s| {
519                s.parse::<u64>().map_err(|err| {
520                    BridgeError::InvalidBridgeClientRequest(format!(
521                        "Invalid token price: {:?}",
522                        err
523                    ))
524                })
525            })
526            .collect::<Result<Vec<_>, _>>()?;
527        let action = BridgeAction::AddTokensOnSuiAction(AddTokensOnSuiAction {
528            chain_id,
529            nonce,
530            native,
531            token_ids,
532            token_type_names,
533            token_prices,
534        });
535        let sig: Json<SignedBridgeAction> = handler.handle_governance_action(action).await?;
536        Ok(sig)
537    };
538    with_metrics!(metrics.clone(), "handle_add_tokens_on_sui", future).await
539}
540
541#[instrument(level = "error", skip_all, fields(chain_id=chain_id, nonce=nonce, native=native, token_ids=token_ids, token_addresses=token_addresses, token_sui_decimals=token_sui_decimals, token_prices=token_prices))]
542async fn handle_add_tokens_on_evm(
543    Path((chain_id, nonce, native, token_ids, token_addresses, token_sui_decimals, token_prices)): Path<(
544        u8,
545        u64,
546        u8,
547        String,
548        String,
549        String,
550        String,
551    )>,
552    State((handler, metrics, _metadata)): State<(
553        Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
554        Arc<BridgeMetrics>,
555        Arc<BridgeNodePublicMetadata>,
556    )>,
557) -> Result<Json<SignedBridgeAction>, BridgeError> {
558    let future = async {
559        let chain_id = BridgeChainId::try_from(chain_id).map_err(|err| {
560            BridgeError::InvalidBridgeClientRequest(format!("Invalid chain id: {:?}", err))
561        })?;
562        if chain_id.is_sui_chain() {
563            return Err(BridgeError::InvalidBridgeClientRequest(
564                "handle_add_tokens_on_evm does not expect Sui chain id".to_string(),
565            ));
566        }
567
568        let native = match native {
569            1 => true,
570            0 => false,
571            _ => {
572                return Err(BridgeError::InvalidBridgeClientRequest(format!(
573                    "Invalid native flag: {}",
574                    native
575                )));
576            }
577        };
578        // Validate list sizes to prevent DoS
579        validate_list_size(&token_ids, "token_ids")?;
580        validate_list_size(&token_addresses, "token_addresses")?;
581        validate_list_size(&token_sui_decimals, "token_sui_decimals")?;
582        validate_list_size(&token_prices, "token_prices")?;
583
584        let token_ids = token_ids
585            .split(',')
586            .map(|s| {
587                s.parse::<u8>().map_err(|err| {
588                    BridgeError::InvalidBridgeClientRequest(format!("Invalid token id: {:?}", err))
589                })
590            })
591            .collect::<Result<Vec<_>, _>>()?;
592        let token_addresses = token_addresses
593            .split(',')
594            .map(|s| {
595                EthAddress::from_str(s).map_err(|err| {
596                    BridgeError::InvalidBridgeClientRequest(format!(
597                        "Invalid token address: {:?}",
598                        err
599                    ))
600                })
601            })
602            .collect::<Result<Vec<_>, _>>()?;
603        let token_sui_decimals = token_sui_decimals
604            .split(',')
605            .map(|s| {
606                s.parse::<u8>().map_err(|err| {
607                    BridgeError::InvalidBridgeClientRequest(format!(
608                        "Invalid token sui decimals: {:?}",
609                        err
610                    ))
611                })
612            })
613            .collect::<Result<Vec<_>, _>>()?;
614        let token_prices = token_prices
615            .split(',')
616            .map(|s| {
617                s.parse::<u64>().map_err(|err| {
618                    BridgeError::InvalidBridgeClientRequest(format!(
619                        "Invalid token price: {:?}",
620                        err
621                    ))
622                })
623            })
624            .collect::<Result<Vec<_>, _>>()?;
625        let action = BridgeAction::AddTokensOnEvmAction(AddTokensOnEvmAction {
626            chain_id,
627            nonce,
628            native,
629            token_ids,
630            token_addresses,
631            token_sui_decimals,
632            token_prices,
633        });
634        let sig: Json<SignedBridgeAction> = handler.handle_governance_action(action).await?;
635        Ok(sig)
636    };
637    with_metrics!(metrics.clone(), "handle_add_tokens_on_evm", future).await
638}
639
640#[macro_export]
641macro_rules! with_metrics {
642    ($metrics:expr, $type_:expr, $func:expr) => {
643        async move {
644            info!("Received {} request", $type_);
645            $metrics
646                .requests_received
647                .with_label_values(&[$type_])
648                .inc();
649            $metrics
650                .requests_inflight
651                .with_label_values(&[$type_])
652                .inc();
653
654            let result = $func.await;
655
656            match &result {
657                Ok(_) => {
658                    info!("{} request succeeded", $type_);
659                    $metrics.requests_ok.with_label_values(&[$type_]).inc();
660                }
661                Err(e) => {
662                    info!("{} request failed: {:?}", $type_, e);
663                    $metrics.err_requests.with_label_values(&[$type_]).inc();
664                }
665            }
666
667            $metrics
668                .requests_inflight
669                .with_label_values(&[$type_])
670                .dec();
671            result
672        }
673    };
674}
675
676#[cfg(test)]
677mod tests {
678    use sui_types::bridge::TOKEN_ID_BTC;
679
680    use super::*;
681    use crate::client::bridge_client::BridgeClient;
682    use crate::server::mock_handler::BridgeRequestMockHandler;
683    use crate::test_utils::get_test_authorities_and_run_mock_bridge_server;
684    use crate::types::BridgeCommittee;
685
686    #[tokio::test]
687    async fn test_bridge_server_handle_blocklist_update_action_path() {
688        let client = setup();
689
690        let pub_key_bytes = BridgeAuthorityPublicKeyBytes::from_bytes(
691            &Hex::decode("02321ede33d2c2d7a8a152f275a1484edef2098f034121a602cb7d767d38680aa4")
692                .unwrap(),
693        )
694        .unwrap();
695        let action = BridgeAction::BlocklistCommitteeAction(BlocklistCommitteeAction {
696            nonce: 129,
697            chain_id: BridgeChainId::SuiCustom,
698            blocklist_type: BlocklistType::Blocklist,
699            members_to_update: vec![pub_key_bytes.clone()],
700        });
701        client.request_sign_bridge_action(action).await.unwrap();
702    }
703
704    #[tokio::test]
705    async fn test_bridge_server_handle_emergency_action_path() {
706        let client = setup();
707
708        let action = BridgeAction::EmergencyAction(EmergencyAction {
709            nonce: 55,
710            chain_id: BridgeChainId::SuiCustom,
711            action_type: EmergencyActionType::Pause,
712        });
713        client.request_sign_bridge_action(action).await.unwrap();
714    }
715
716    #[tokio::test]
717    async fn test_bridge_server_handle_limit_update_action_path() {
718        let client = setup();
719
720        let action = BridgeAction::LimitUpdateAction(LimitUpdateAction {
721            nonce: 15,
722            chain_id: BridgeChainId::SuiCustom,
723            sending_chain_id: BridgeChainId::EthCustom,
724            new_usd_limit: 1_000_000_0000, // $1M USD
725        });
726        client.request_sign_bridge_action(action).await.unwrap();
727    }
728
729    #[tokio::test]
730    async fn test_bridge_server_handle_asset_price_update_action_path() {
731        let client = setup();
732
733        let action = BridgeAction::AssetPriceUpdateAction(AssetPriceUpdateAction {
734            nonce: 266,
735            chain_id: BridgeChainId::SuiCustom,
736            token_id: TOKEN_ID_BTC,
737            new_usd_price: 100_000_0000, // $100k USD
738        });
739        client.request_sign_bridge_action(action).await.unwrap();
740    }
741
742    #[tokio::test]
743    async fn test_bridge_server_handle_evm_contract_upgrade_action_path() {
744        let client = setup();
745
746        let action = BridgeAction::EvmContractUpgradeAction(EvmContractUpgradeAction {
747            nonce: 123,
748            chain_id: BridgeChainId::EthCustom,
749            proxy_address: EthAddress::repeat_byte(6),
750            new_impl_address: EthAddress::repeat_byte(9),
751            call_data: vec![],
752        });
753        client.request_sign_bridge_action(action).await.unwrap();
754
755        let action = BridgeAction::EvmContractUpgradeAction(EvmContractUpgradeAction {
756            nonce: 123,
757            chain_id: BridgeChainId::EthCustom,
758            proxy_address: EthAddress::repeat_byte(6),
759            new_impl_address: EthAddress::repeat_byte(9),
760            call_data: vec![12, 34, 56],
761        });
762        client.request_sign_bridge_action(action).await.unwrap();
763    }
764
765    #[tokio::test]
766    async fn test_bridge_server_handle_add_tokens_on_sui_action_path() {
767        let client = setup();
768
769        let action = BridgeAction::AddTokensOnSuiAction(AddTokensOnSuiAction {
770            nonce: 266,
771            chain_id: BridgeChainId::SuiCustom,
772            native: false,
773            token_ids: vec![100, 101, 102],
774            token_type_names: vec![
775                TypeTag::from_str("0x0000000000000000000000000000000000000000000000000000000000000abc::my_coin::MyCoin1").unwrap(),
776                TypeTag::from_str("0x0000000000000000000000000000000000000000000000000000000000000abc::my_coin::MyCoin2").unwrap(),
777                TypeTag::from_str("0x0000000000000000000000000000000000000000000000000000000000000abc::my_coin::MyCoin3").unwrap(),
778            ],
779            token_prices: vec![100_000_0000, 200_000_0000, 300_000_0000],
780        });
781        client.request_sign_bridge_action(action).await.unwrap();
782    }
783
784    #[tokio::test]
785    async fn test_bridge_server_handle_add_tokens_on_evm_action_path() {
786        let client = setup();
787
788        let action = BridgeAction::AddTokensOnEvmAction(crate::types::AddTokensOnEvmAction {
789            nonce: 0,
790            chain_id: BridgeChainId::EthCustom,
791            native: false,
792            token_ids: vec![99, 100, 101],
793            token_addresses: vec![
794                EthAddress::repeat_byte(1),
795                EthAddress::repeat_byte(2),
796                EthAddress::repeat_byte(3),
797            ],
798            token_sui_decimals: vec![5, 6, 7],
799            token_prices: vec![1_000_000_000, 2_000_000_000, 3_000_000_000],
800        });
801        client.request_sign_bridge_action(action).await.unwrap();
802    }
803
804    fn setup() -> BridgeClient {
805        let mock = BridgeRequestMockHandler::new();
806        let (_handles, authorities, mut secrets) =
807            get_test_authorities_and_run_mock_bridge_server(vec![10000], vec![mock.clone()]);
808        mock.set_signer(secrets.swap_remove(0));
809        let committee = BridgeCommittee::new(authorities).unwrap();
810        let pub_key = committee.members().keys().next().unwrap();
811        BridgeClient::new(pub_key.clone(), Arc::new(committee)).unwrap()
812    }
813}