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