1#![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, 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
44pub const MAX_LIST_SIZE: usize = 255;
47
48pub const PING_PATH: &str = "/ping";
49pub const METRICS_KEY_PATH: &str = "/metrics_pub_key";
50
51pub 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#[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(middleware::from_fn(reject_oversized_requests))
142 .with_state((handler, metrics, metadata))
143}
144
145async fn reject_oversized_requests(req: Request, next: Next) -> Response {
146 let uri_len = req
147 .uri()
148 .path_and_query()
149 .map(|v| v.as_str().len())
150 .unwrap_or(0);
151 if uri_len > MAX_REQUEST_URI_SIZE {
152 return StatusCode::URI_TOO_LONG.into_response();
153 }
154
155 if let Some(content_length) = req.headers().get(axum::http::header::CONTENT_LENGTH) {
156 let body_len = content_length
157 .to_str()
158 .ok()
159 .and_then(|v| v.parse::<usize>().ok());
160
161 match body_len {
162 Some(size) if size > MAX_REQUEST_BODY_SIZE => {
163 return StatusCode::PAYLOAD_TOO_LARGE.into_response();
164 }
165 None => {
166 return StatusCode::BAD_REQUEST.into_response();
167 }
168 _ => {}
169 }
170 }
171
172 next.run(req).await
173}
174
175impl axum::response::IntoResponse for BridgeError {
176 fn into_response(self) -> axum::response::Response {
177 let status = match &self {
178 BridgeError::InvalidTxHash
179 | BridgeError::UnknownTokenId(_)
180 | BridgeError::InvalidBridgeClientRequest(_)
181 | BridgeError::InvalidChainId
182 | BridgeError::ActionIsNotGovernanceAction(_)
183 | BridgeError::ActionIsNotTokenTransferAction
184 | BridgeError::GovernanceActionIsNotApproved => StatusCode::BAD_REQUEST,
185 BridgeError::TxNotFound | BridgeError::NoBridgeEventsInTxPosition => {
186 StatusCode::NOT_FOUND
187 }
188 BridgeError::TxNotFinalized => StatusCode::CONFLICT,
189 BridgeError::TransientProviderError(_) => StatusCode::SERVICE_UNAVAILABLE,
190 _ => StatusCode::INTERNAL_SERVER_ERROR,
191 };
192
193 let sanitized_error = match self {
194 BridgeError::InvalidTxHash => "InvalidTxHash",
195 BridgeError::OriginTxFailed => "OriginTxFailed",
196 BridgeError::TxNotFound => "TxNotFound",
197 BridgeError::TxNotFinalized => "TxNotFinalized",
198 BridgeError::NoBridgeEventsInTxPosition => "NoBridgeEventsInTxPosition",
199 BridgeError::BridgeEventInUnrecognizedEthContract => {
200 "BridgeEventInUnrecognizedEthContract"
201 }
202 BridgeError::BridgeEventInUnrecognizedSuiPackage => {
203 "BridgeEventInUnrecognizedSuiPackage"
204 }
205 BridgeError::BridgeEventNotActionable => "BridgeEventNotActionable",
206 BridgeError::UnknownTokenId(_) => "UnknownTokenId",
207 BridgeError::InvalidBridgeCommittee(_) => "InvalidBridgeCommittee",
208 BridgeError::InvalidBridgeAuthoritySignature(_) => "InvalidBridgeAuthoritySignature",
209 BridgeError::InvalidBridgeAuthority(_) => "InvalidBridgeAuthority",
210 BridgeError::InvalidAuthorityUrl(_) => "InvalidAuthorityUrl",
211 BridgeError::InvalidBridgeClientRequest(_) => "InvalidBridgeClientRequest",
212 BridgeError::InvalidChainId => "InvalidChainId",
213 BridgeError::MismatchedAuthoritySigner => "MismatchedAuthoritySigner",
214 BridgeError::MismatchedAction => "MismatchedAction",
215 BridgeError::ActionIsNotGovernanceAction(_) => "ActionIsNotGovernanceAction",
216 BridgeError::GovernanceActionIsNotApproved => "GovernanceActionIsNotApproved",
217 BridgeError::AuthoirtyUrlInvalid => "AuthoirtyUrlInvalid",
218 BridgeError::ActionIsNotTokenTransferAction => "ActionIsNotTokenTransferAction",
219 BridgeError::TransientProviderError(_) => "TransientProviderError",
220 _ => "InternalError",
221 };
222
223 (status, format!("BridgeError::{sanitized_error}")).into_response()
224 }
225}
226
227impl<E> From<E> for BridgeError
228where
229 E: Into<anyhow::Error>,
230{
231 fn from(err: E) -> Self {
232 Self::Generic(err.into().to_string())
233 }
234}
235
236async fn health_check() -> StatusCode {
237 StatusCode::OK
238}
239
240fn validate_list_size(list_str: &str, field_name: &str) -> Result<(), BridgeError> {
243 let count = list_str.split(',').count();
244 if count > MAX_LIST_SIZE {
245 return Err(BridgeError::InvalidBridgeClientRequest(format!(
246 "{} list size {} exceeds maximum allowed size of {}",
247 field_name, count, MAX_LIST_SIZE
248 )));
249 }
250 Ok(())
251}
252
253async fn ping(
254 State((_handler, _metrics, metadata)): State<(
255 Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
256 Arc<BridgeMetrics>,
257 Arc<BridgeNodePublicMetadata>,
258 )>,
259) -> Result<Json<Arc<BridgeNodePublicMetadata>>, BridgeError> {
260 Ok(Json(metadata))
261}
262
263async fn metrics_key_fetch(
264 State((_handler, _metrics, metadata)): State<(
265 Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
266 Arc<BridgeMetrics>,
267 Arc<BridgeNodePublicMetadata>,
268 )>,
269) -> Result<Json<Option<Arc<Ed25519PublicKey>>>, BridgeError> {
270 Ok(Json(metadata.metrics_pubkey.clone()))
271}
272
273#[instrument(level = "error", skip_all, fields(tx_hash_hex=tx_hash_hex, event_idx=event_idx))]
274async fn handle_eth_tx_hash(
275 Path((tx_hash_hex, 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 = handler.handle_eth_tx_hash(tx_hash_hex, event_idx).await?;
284 Ok(sig)
285 };
286 with_metrics!(metrics.clone(), "handle_eth_tx_hash", future).await
287}
288
289#[instrument(level = "error", skip_all, fields(tx_digest_base58=tx_digest_base58, event_idx=event_idx))]
290async fn handle_sui_tx_digest(
291 Path((tx_digest_base58, event_idx)): Path<(String, u16)>,
292 State((handler, metrics, _metadata)): State<(
293 Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
294 Arc<BridgeMetrics>,
295 Arc<BridgeNodePublicMetadata>,
296 )>,
297) -> Result<Json<SignedBridgeAction>, BridgeError> {
298 let future = async {
299 let sig: Json<SignedBridgeAction> = handler
300 .handle_sui_tx_digest(tx_digest_base58, event_idx)
301 .await?;
302 Ok(sig)
303 };
304 with_metrics!(metrics.clone(), "handle_sui_tx_digest", future).await
305}
306
307#[instrument(level = "error", skip_all, fields(source_chain=source_chain, message_type=message_type, bridge_seq_num=bridge_seq_num))]
308async fn handle_sui_token_transfer(
309 Path((source_chain, message_type, bridge_seq_num)): Path<(u8, u8, u64)>,
310 State((handler, metrics, _metadata)): State<(
311 Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
312 Arc<BridgeMetrics>,
313 Arc<BridgeNodePublicMetadata>,
314 )>,
315) -> Result<Json<SignedBridgeAction>, BridgeError> {
316 let future = async {
317 let sig: Json<SignedBridgeAction> = handler
318 .handle_sui_token_transfer(source_chain, message_type, bridge_seq_num)
319 .await?;
320 Ok(sig)
321 };
322 with_metrics!(metrics.clone(), "handle_sui_token_transfer", future).await
323}
324
325#[instrument(level = "error", skip_all, fields(chain_id=chain_id, nonce=nonce, blocklist_type=blocklist_type, keys=keys))]
326async fn handle_update_committee_blocklist_action(
327 Path((chain_id, nonce, blocklist_type, keys)): Path<(u8, u64, u8, String)>,
328 State((handler, metrics, _metadata)): State<(
329 Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
330 Arc<BridgeMetrics>,
331 Arc<BridgeNodePublicMetadata>,
332 )>,
333) -> Result<Json<SignedBridgeAction>, BridgeError> {
334 let future = async {
335 let chain_id = BridgeChainId::try_from(chain_id).map_err(|err| {
336 BridgeError::InvalidBridgeClientRequest(format!("Invalid chain id: {:?}", err))
337 })?;
338 let blocklist_type = BlocklistType::try_from(blocklist_type).map_err(|err| {
339 BridgeError::InvalidBridgeClientRequest(format!(
340 "Invalid blocklist action type: {:?}",
341 err
342 ))
343 })?;
344 validate_list_size(&keys, "keys")?;
346 let members_to_update = keys
347 .split(',')
348 .map(|s| {
349 let bytes = Hex::decode(s).map_err(|e| anyhow::anyhow!("{:?}", e))?;
350 BridgeAuthorityPublicKeyBytes::from_bytes(&bytes)
351 .map_err(|e| anyhow::anyhow!("{:?}", e))
352 })
353 .collect::<Result<Vec<_>, _>>()
354 .map_err(|e| BridgeError::InvalidBridgeClientRequest(format!("{:?}", e)))?;
355 let action = BridgeAction::BlocklistCommitteeAction(BlocklistCommitteeAction {
356 chain_id,
357 nonce,
358 blocklist_type,
359 members_to_update,
360 });
361
362 let sig: Json<SignedBridgeAction> = handler.handle_governance_action(action).await?;
363 Ok(sig)
364 };
365 with_metrics!(
366 metrics.clone(),
367 "handle_update_committee_blocklist_action",
368 future
369 )
370 .await
371}
372
373#[instrument(level = "error", skip_all, fields(chain_id=chain_id, nonce=nonce, action_type=action_type))]
374async fn handle_emergency_action(
375 Path((chain_id, nonce, action_type)): Path<(u8, u64, u8)>,
376 State((handler, metrics, _metadata)): State<(
377 Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
378 Arc<BridgeMetrics>,
379 Arc<BridgeNodePublicMetadata>,
380 )>,
381) -> Result<Json<SignedBridgeAction>, BridgeError> {
382 let future = async {
383 let chain_id = BridgeChainId::try_from(chain_id).map_err(|err| {
384 BridgeError::InvalidBridgeClientRequest(format!("Invalid chain id: {:?}", err))
385 })?;
386 let action_type = EmergencyActionType::try_from(action_type).map_err(|err| {
387 BridgeError::InvalidBridgeClientRequest(format!(
388 "Invalid emergency action type: {:?}",
389 err
390 ))
391 })?;
392 let action = BridgeAction::EmergencyAction(EmergencyAction {
393 chain_id,
394 nonce,
395 action_type,
396 });
397 let sig: Json<SignedBridgeAction> = handler.handle_governance_action(action).await?;
398 Ok(sig)
399 };
400 with_metrics!(metrics.clone(), "handle_emergency_action", future).await
401}
402
403#[instrument(level = "error", skip_all, fields(chain_id=chain_id, nonce=nonce, sending_chain_id=sending_chain_id, new_usd_limit=new_usd_limit))]
404async fn handle_limit_update_action(
405 Path((chain_id, nonce, sending_chain_id, new_usd_limit)): Path<(u8, u64, u8, u64)>,
406 State((handler, metrics, _metadata)): State<(
407 Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
408 Arc<BridgeMetrics>,
409 Arc<BridgeNodePublicMetadata>,
410 )>,
411) -> Result<Json<SignedBridgeAction>, BridgeError> {
412 let future = async {
413 let chain_id = BridgeChainId::try_from(chain_id).map_err(|err| {
414 BridgeError::InvalidBridgeClientRequest(format!("Invalid chain id: {:?}", err))
415 })?;
416 let sending_chain_id = BridgeChainId::try_from(sending_chain_id).map_err(|err| {
417 BridgeError::InvalidBridgeClientRequest(format!("Invalid chain id: {:?}", err))
418 })?;
419 let action = BridgeAction::LimitUpdateAction(LimitUpdateAction {
420 chain_id,
421 nonce,
422 sending_chain_id,
423 new_usd_limit,
424 });
425 let sig: Json<SignedBridgeAction> = handler.handle_governance_action(action).await?;
426 Ok(sig)
427 };
428 with_metrics!(metrics.clone(), "handle_limit_update_action", future).await
429}
430
431#[instrument(level = "error", skip_all, fields(chain_id=chain_id, nonce=nonce, token_id=token_id, new_usd_price=new_usd_price))]
432async fn handle_asset_price_update_action(
433 Path((chain_id, nonce, token_id, new_usd_price)): Path<(u8, u64, u8, u64)>,
434 State((handler, metrics, _metadata)): State<(
435 Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
436 Arc<BridgeMetrics>,
437 Arc<BridgeNodePublicMetadata>,
438 )>,
439) -> Result<Json<SignedBridgeAction>, BridgeError> {
440 let future = async {
441 let chain_id = BridgeChainId::try_from(chain_id).map_err(|err| {
442 BridgeError::InvalidBridgeClientRequest(format!("Invalid chain id: {:?}", err))
443 })?;
444 let action = BridgeAction::AssetPriceUpdateAction(AssetPriceUpdateAction {
445 chain_id,
446 nonce,
447 token_id,
448 new_usd_price,
449 });
450 let sig: Json<SignedBridgeAction> = handler.handle_governance_action(action).await?;
451 Ok(sig)
452 };
453 with_metrics!(metrics.clone(), "handle_asset_price_update_action", future).await
454}
455
456#[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)))]
457async fn handle_evm_contract_upgrade_with_calldata(
458 Path((chain_id, nonce, proxy_address, new_impl_address, calldata)): Path<(
459 u8,
460 u64,
461 EthAddress,
462 EthAddress,
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 let call_data = Hex::decode(&calldata).map_err(|e| {
476 BridgeError::InvalidBridgeClientRequest(format!("Invalid call data: {:?}", e))
477 })?;
478 let action = BridgeAction::EvmContractUpgradeAction(EvmContractUpgradeAction {
479 chain_id,
480 nonce,
481 proxy_address,
482 new_impl_address,
483 call_data,
484 });
485 let sig: Json<SignedBridgeAction> = handler.handle_governance_action(action).await?;
486 Ok(sig)
487 };
488 with_metrics!(
489 metrics.clone(),
490 "handle_evm_contract_upgrade_with_calldata",
491 future
492 )
493 .await
494}
495
496#[instrument(
497 level = "error",
498 skip_all,
499 fields(chain_id, nonce, proxy_address, new_impl_address)
500)]
501async fn handle_evm_contract_upgrade(
502 Path((chain_id, nonce, proxy_address, new_impl_address)): Path<(
503 u8,
504 u64,
505 EthAddress,
506 EthAddress,
507 )>,
508 State((handler, metrics, _metadata)): State<(
509 Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
510 Arc<BridgeMetrics>,
511 Arc<BridgeNodePublicMetadata>,
512 )>,
513) -> Result<Json<SignedBridgeAction>, BridgeError> {
514 let future = async {
515 let chain_id = BridgeChainId::try_from(chain_id).map_err(|err| {
516 BridgeError::InvalidBridgeClientRequest(format!("Invalid chain id: {:?}", err))
517 })?;
518 let action = BridgeAction::EvmContractUpgradeAction(EvmContractUpgradeAction {
519 chain_id,
520 nonce,
521 proxy_address,
522 new_impl_address,
523 call_data: vec![],
524 });
525 let sig: Json<SignedBridgeAction> = handler.handle_governance_action(action).await?;
526
527 Ok(sig)
528 };
529 with_metrics!(metrics.clone(), "handle_evm_contract_upgrade", future).await
530}
531
532#[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))]
533async fn handle_add_tokens_on_sui(
534 Path((chain_id, nonce, native, token_ids, token_type_names, token_prices)): Path<(
535 u8,
536 u64,
537 u8,
538 String,
539 String,
540 String,
541 )>,
542 State((handler, metrics, _metadata)): State<(
543 Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
544 Arc<BridgeMetrics>,
545 Arc<BridgeNodePublicMetadata>,
546 )>,
547) -> Result<Json<SignedBridgeAction>, BridgeError> {
548 let future = async {
549 let chain_id = BridgeChainId::try_from(chain_id).map_err(|err| {
550 BridgeError::InvalidBridgeClientRequest(format!("Invalid chain id: {:?}", err))
551 })?;
552
553 if !chain_id.is_sui_chain() {
554 return Err(BridgeError::InvalidBridgeClientRequest(
555 "handle_add_tokens_on_sui only expects Sui chain id".to_string(),
556 ));
557 }
558
559 let native = match native {
560 1 => true,
561 0 => false,
562 _ => {
563 return Err(BridgeError::InvalidBridgeClientRequest(format!(
564 "Invalid native flag: {}",
565 native
566 )));
567 }
568 };
569 validate_list_size(&token_ids, "token_ids")?;
571 validate_list_size(&token_type_names, "token_type_names")?;
572 validate_list_size(&token_prices, "token_prices")?;
573
574 let token_ids = token_ids
575 .split(',')
576 .map(|s| {
577 s.parse::<u8>().map_err(|err| {
578 BridgeError::InvalidBridgeClientRequest(format!("Invalid token id: {:?}", err))
579 })
580 })
581 .collect::<Result<Vec<_>, _>>()?;
582 let token_type_names = token_type_names
583 .split(',')
584 .map(|s| {
585 TypeTag::from_str(s).map_err(|err| {
586 BridgeError::InvalidBridgeClientRequest(format!(
587 "Invalid token type name: {:?}",
588 err
589 ))
590 })
591 })
592 .collect::<Result<Vec<_>, _>>()?;
593 let token_prices = token_prices
594 .split(',')
595 .map(|s| {
596 s.parse::<u64>().map_err(|err| {
597 BridgeError::InvalidBridgeClientRequest(format!(
598 "Invalid token price: {:?}",
599 err
600 ))
601 })
602 })
603 .collect::<Result<Vec<_>, _>>()?;
604 let action = BridgeAction::AddTokensOnSuiAction(AddTokensOnSuiAction {
605 chain_id,
606 nonce,
607 native,
608 token_ids,
609 token_type_names,
610 token_prices,
611 });
612 let sig: Json<SignedBridgeAction> = handler.handle_governance_action(action).await?;
613 Ok(sig)
614 };
615 with_metrics!(metrics.clone(), "handle_add_tokens_on_sui", future).await
616}
617
618#[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))]
619async fn handle_add_tokens_on_evm(
620 Path((chain_id, nonce, native, token_ids, token_addresses, token_sui_decimals, token_prices)): Path<(
621 u8,
622 u64,
623 u8,
624 String,
625 String,
626 String,
627 String,
628 )>,
629 State((handler, metrics, _metadata)): State<(
630 Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
631 Arc<BridgeMetrics>,
632 Arc<BridgeNodePublicMetadata>,
633 )>,
634) -> Result<Json<SignedBridgeAction>, BridgeError> {
635 let future = async {
636 let chain_id = BridgeChainId::try_from(chain_id).map_err(|err| {
637 BridgeError::InvalidBridgeClientRequest(format!("Invalid chain id: {:?}", err))
638 })?;
639 if chain_id.is_sui_chain() {
640 return Err(BridgeError::InvalidBridgeClientRequest(
641 "handle_add_tokens_on_evm does not expect Sui chain id".to_string(),
642 ));
643 }
644
645 let native = match native {
646 1 => true,
647 0 => false,
648 _ => {
649 return Err(BridgeError::InvalidBridgeClientRequest(format!(
650 "Invalid native flag: {}",
651 native
652 )));
653 }
654 };
655 validate_list_size(&token_ids, "token_ids")?;
657 validate_list_size(&token_addresses, "token_addresses")?;
658 validate_list_size(&token_sui_decimals, "token_sui_decimals")?;
659 validate_list_size(&token_prices, "token_prices")?;
660
661 let token_ids = token_ids
662 .split(',')
663 .map(|s| {
664 s.parse::<u8>().map_err(|err| {
665 BridgeError::InvalidBridgeClientRequest(format!("Invalid token id: {:?}", err))
666 })
667 })
668 .collect::<Result<Vec<_>, _>>()?;
669 let token_addresses = token_addresses
670 .split(',')
671 .map(|s| {
672 EthAddress::from_str(s).map_err(|err| {
673 BridgeError::InvalidBridgeClientRequest(format!(
674 "Invalid token address: {:?}",
675 err
676 ))
677 })
678 })
679 .collect::<Result<Vec<_>, _>>()?;
680 let token_sui_decimals = token_sui_decimals
681 .split(',')
682 .map(|s| {
683 s.parse::<u8>().map_err(|err| {
684 BridgeError::InvalidBridgeClientRequest(format!(
685 "Invalid token sui decimals: {:?}",
686 err
687 ))
688 })
689 })
690 .collect::<Result<Vec<_>, _>>()?;
691 let token_prices = token_prices
692 .split(',')
693 .map(|s| {
694 s.parse::<u64>().map_err(|err| {
695 BridgeError::InvalidBridgeClientRequest(format!(
696 "Invalid token price: {:?}",
697 err
698 ))
699 })
700 })
701 .collect::<Result<Vec<_>, _>>()?;
702 let action = BridgeAction::AddTokensOnEvmAction(AddTokensOnEvmAction {
703 chain_id,
704 nonce,
705 native,
706 token_ids,
707 token_addresses,
708 token_sui_decimals,
709 token_prices,
710 });
711 let sig: Json<SignedBridgeAction> = handler.handle_governance_action(action).await?;
712 Ok(sig)
713 };
714 with_metrics!(metrics.clone(), "handle_add_tokens_on_evm", future).await
715}
716
717#[macro_export]
718macro_rules! with_metrics {
719 ($metrics:expr, $type_:expr, $func:expr) => {
720 async move {
721 info!("Received {} request", $type_);
722 $metrics
723 .requests_received
724 .with_label_values(&[$type_])
725 .inc();
726 $metrics
727 .requests_inflight
728 .with_label_values(&[$type_])
729 .inc();
730
731 let result = $func.await;
732
733 match &result {
734 Ok(_) => {
735 info!("{} request succeeded", $type_);
736 $metrics.requests_ok.with_label_values(&[$type_]).inc();
737 }
738 Err(e) => {
739 info!("{} request failed: {:?}", $type_, e);
740 $metrics.err_requests.with_label_values(&[$type_]).inc();
741 }
742 }
743
744 $metrics
745 .requests_inflight
746 .with_label_values(&[$type_])
747 .dec();
748 result
749 }
750 };
751}
752
753#[cfg(test)]
754mod tests {
755 use sui_types::bridge::TOKEN_ID_BTC;
756
757 use super::*;
758 use crate::client::bridge_client::BridgeClient;
759 use crate::server::mock_handler::BridgeRequestMockHandler;
760 use crate::test_utils::get_test_authorities_and_run_mock_bridge_server;
761 use crate::types::BridgeCommittee;
762 use axum::response::IntoResponse;
763 use reqwest::header::CONTENT_LENGTH;
764
765 #[tokio::test]
766 async fn test_bridge_server_handle_blocklist_update_action_path() {
767 let client = setup();
768
769 let pub_key_bytes = BridgeAuthorityPublicKeyBytes::from_bytes(
770 &Hex::decode("02321ede33d2c2d7a8a152f275a1484edef2098f034121a602cb7d767d38680aa4")
771 .unwrap(),
772 )
773 .unwrap();
774 let action = BridgeAction::BlocklistCommitteeAction(BlocklistCommitteeAction {
775 nonce: 129,
776 chain_id: BridgeChainId::SuiCustom,
777 blocklist_type: BlocklistType::Blocklist,
778 members_to_update: vec![pub_key_bytes.clone()],
779 });
780 client.request_sign_bridge_action(action).await.unwrap();
781 }
782
783 #[tokio::test]
784 async fn test_bridge_server_handle_emergency_action_path() {
785 let client = setup();
786
787 let action = BridgeAction::EmergencyAction(EmergencyAction {
788 nonce: 55,
789 chain_id: BridgeChainId::SuiCustom,
790 action_type: EmergencyActionType::Pause,
791 });
792 client.request_sign_bridge_action(action).await.unwrap();
793 }
794
795 #[tokio::test]
796 async fn test_bridge_server_handle_limit_update_action_path() {
797 let client = setup();
798
799 let action = BridgeAction::LimitUpdateAction(LimitUpdateAction {
800 nonce: 15,
801 chain_id: BridgeChainId::SuiCustom,
802 sending_chain_id: BridgeChainId::EthCustom,
803 new_usd_limit: 1_000_000_0000, });
805 client.request_sign_bridge_action(action).await.unwrap();
806 }
807
808 #[tokio::test]
809 async fn test_bridge_server_handle_asset_price_update_action_path() {
810 let client = setup();
811
812 let action = BridgeAction::AssetPriceUpdateAction(AssetPriceUpdateAction {
813 nonce: 266,
814 chain_id: BridgeChainId::SuiCustom,
815 token_id: TOKEN_ID_BTC,
816 new_usd_price: 100_000_0000, });
818 client.request_sign_bridge_action(action).await.unwrap();
819 }
820
821 #[tokio::test]
822 async fn test_bridge_server_handle_evm_contract_upgrade_action_path() {
823 let client = setup();
824
825 let action = BridgeAction::EvmContractUpgradeAction(EvmContractUpgradeAction {
826 nonce: 123,
827 chain_id: BridgeChainId::EthCustom,
828 proxy_address: EthAddress::repeat_byte(6),
829 new_impl_address: EthAddress::repeat_byte(9),
830 call_data: vec![],
831 });
832 client.request_sign_bridge_action(action).await.unwrap();
833
834 let action = BridgeAction::EvmContractUpgradeAction(EvmContractUpgradeAction {
835 nonce: 123,
836 chain_id: BridgeChainId::EthCustom,
837 proxy_address: EthAddress::repeat_byte(6),
838 new_impl_address: EthAddress::repeat_byte(9),
839 call_data: vec![12, 34, 56],
840 });
841 client.request_sign_bridge_action(action).await.unwrap();
842 }
843
844 #[tokio::test]
845 async fn test_bridge_server_handle_add_tokens_on_sui_action_path() {
846 let client = setup();
847
848 let action = BridgeAction::AddTokensOnSuiAction(AddTokensOnSuiAction {
849 nonce: 266,
850 chain_id: BridgeChainId::SuiCustom,
851 native: false,
852 token_ids: vec![100, 101, 102],
853 token_type_names: vec![
854 TypeTag::from_str("0x0000000000000000000000000000000000000000000000000000000000000abc::my_coin::MyCoin1").unwrap(),
855 TypeTag::from_str("0x0000000000000000000000000000000000000000000000000000000000000abc::my_coin::MyCoin2").unwrap(),
856 TypeTag::from_str("0x0000000000000000000000000000000000000000000000000000000000000abc::my_coin::MyCoin3").unwrap(),
857 ],
858 token_prices: vec![100_000_0000, 200_000_0000, 300_000_0000],
859 });
860 client.request_sign_bridge_action(action).await.unwrap();
861 }
862
863 #[tokio::test]
864 async fn test_bridge_server_handle_add_tokens_on_evm_action_path() {
865 let client = setup();
866
867 let action = BridgeAction::AddTokensOnEvmAction(crate::types::AddTokensOnEvmAction {
868 nonce: 0,
869 chain_id: BridgeChainId::EthCustom,
870 native: false,
871 token_ids: vec![99, 100, 101],
872 token_addresses: vec![
873 EthAddress::repeat_byte(1),
874 EthAddress::repeat_byte(2),
875 EthAddress::repeat_byte(3),
876 ],
877 token_sui_decimals: vec![5, 6, 7],
878 token_prices: vec![1_000_000_000, 2_000_000_000, 3_000_000_000],
879 });
880 client.request_sign_bridge_action(action).await.unwrap();
881 }
882
883 #[tokio::test]
884 async fn test_bridge_server_rejects_oversized_uri() {
885 let mock = BridgeRequestMockHandler::new();
886 let (_handles, ports) = crate::test_utils::run_mock_bridge_server(vec![mock]);
887 let port = ports[0];
888
889 let oversized_query = "a".repeat(MAX_REQUEST_URI_SIZE + 1);
890 let response = reqwest::Client::new()
891 .get(format!("http://127.0.0.1:{port}/ping?{oversized_query}"))
892 .send()
893 .await
894 .unwrap();
895
896 assert_eq!(response.status(), StatusCode::URI_TOO_LONG);
897 }
898
899 #[tokio::test]
900 async fn test_bridge_server_rejects_oversized_body() {
901 let mock = BridgeRequestMockHandler::new();
902 let (_handles, ports) = crate::test_utils::run_mock_bridge_server(vec![mock]);
903 let port = ports[0];
904
905 let oversized_body = "a".repeat(MAX_REQUEST_BODY_SIZE + 1);
906 let response = reqwest::Client::new()
907 .get(format!("http://127.0.0.1:{port}/ping"))
908 .header(CONTENT_LENGTH, oversized_body.len())
909 .body(oversized_body)
910 .send()
911 .await
912 .unwrap();
913
914 assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE);
915 }
916
917 fn setup() -> BridgeClient {
918 let mock = BridgeRequestMockHandler::new();
919 let (_handles, authorities, mut secrets) =
920 get_test_authorities_and_run_mock_bridge_server(vec![10000], vec![mock.clone()]);
921 mock.set_signer(secrets.swap_remove(0));
922 let committee = BridgeCommittee::new(authorities).unwrap();
923 let pub_key = committee.members().keys().next().unwrap();
924 BridgeClient::new(pub_key.clone(), Arc::new(committee)).unwrap()
925 }
926
927 #[tokio::test]
928 async fn test_bridge_error_response_is_sanitized() {
929 let response = BridgeError::Generic("sensitive server detail".to_string()).into_response();
930 assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
931
932 let body = axum::body::to_bytes(response.into_body(), usize::MAX)
933 .await
934 .unwrap();
935 let body = String::from_utf8(body.to_vec()).unwrap();
936 assert_eq!(body, "BridgeError::InternalError");
937 assert!(!body.contains("sensitive server detail"));
938 }
939}