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