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::{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
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(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
224fn 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(&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_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_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, });
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, });
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}