1use crate::crypto::{BridgeAuthorityPublicKeyBytes, verify_signed_bridge_action};
7use crate::error::{BridgeError, BridgeResult};
8use crate::server::APPLICATION_JSON;
9use crate::types::{BridgeAction, BridgeCommittee, SignedBridgeAction, VerifiedSignedBridgeAction};
10use fastcrypto::encoding::{Encoding, Hex};
11use fastcrypto::traits::ToFromBytes;
12use std::str::FromStr;
13use std::sync::Arc;
14use url::Url;
15
16const MAX_BRIDGE_CLIENT_RESPONSE_SIZE: usize = 1024 * 1024;
17
18#[derive(Clone, Debug)]
24pub struct BridgeClient {
25 inner: reqwest::Client,
26 authority: BridgeAuthorityPublicKeyBytes,
27 committee: Arc<BridgeCommittee>,
28 base_url: Option<Url>,
29}
30
31impl BridgeClient {
32 pub fn new(
33 authority_name: BridgeAuthorityPublicKeyBytes,
34 committee: Arc<BridgeCommittee>,
35 ) -> BridgeResult<Self> {
36 if !committee.is_active_member(&authority_name) {
37 return Err(BridgeError::InvalidBridgeAuthority(authority_name));
38 }
39 let member = committee.member(&authority_name).unwrap();
41 Ok(Self {
42 inner: reqwest::Client::builder()
43 .redirect(reqwest::redirect::Policy::none())
44 .timeout(std::time::Duration::from_secs(30))
45 .build()?,
46 authority: authority_name.clone(),
47 base_url: Url::from_str(&member.base_url).ok(),
48 committee,
49 })
50 }
51
52 #[cfg(test)]
53 pub fn update_committee(&mut self, committee: Arc<BridgeCommittee>) {
54 self.committee = committee;
55 }
56
57 fn bridge_action_to_path(event: &BridgeAction) -> String {
59 match event {
60 BridgeAction::SuiToEthBridgeAction(e) => format!(
61 "sign/bridge_tx/sui/eth/{}/{}",
62 e.sui_tx_digest, e.sui_tx_event_index
63 ),
64 BridgeAction::SuiToEthTokenTransfer(_) | BridgeAction::SuiToEthTokenTransferV2(_) => {
65 format!(
66 "/sign/bridge_action/sui/eth/{source_chain}/{message_type}/{bridge_seq_num}",
67 source_chain = event.chain_id() as u8,
68 message_type = event.action_type() as u8,
69 bridge_seq_num = event.seq_number(),
70 )
71 }
72 BridgeAction::EthToSuiBridgeAction(e) => format!(
73 "sign/bridge_tx/eth/sui/{}/{}",
74 Hex::encode(e.eth_tx_hash.0),
75 e.eth_event_index
76 ),
77 BridgeAction::EthToSuiTokenTransferV2(e) => format!(
78 "sign/bridge_tx/eth/sui/{}/{}",
79 Hex::encode(e.eth_tx_hash.0),
80 e.eth_event_index
81 ),
82 BridgeAction::BlocklistCommitteeAction(a) => {
83 let chain_id = (a.chain_id as u8).to_string();
84 let nonce = a.nonce.to_string();
85 let type_ = (a.blocklist_type as u8).to_string();
86 let keys = a
87 .members_to_update
88 .iter()
89 .map(|k| Hex::encode(k.as_bytes()))
90 .collect::<Vec<_>>()
91 .join(",");
92 format!("sign/update_committee_blocklist/{chain_id}/{nonce}/{type_}/{keys}")
93 }
94 BridgeAction::EmergencyAction(a) => {
95 let chain_id = (a.chain_id as u8).to_string();
96 let nonce = a.nonce.to_string();
97 let type_ = (a.action_type as u8).to_string();
98 format!("sign/emergency_button/{chain_id}/{nonce}/{type_}")
99 }
100 BridgeAction::LimitUpdateAction(a) => {
101 let chain_id = (a.chain_id as u8).to_string();
102 let nonce = a.nonce.to_string();
103 let sending_chain_id = (a.sending_chain_id as u8).to_string();
104 let new_usd_limit = a.new_usd_limit.to_string();
105 format!("sign/update_limit/{chain_id}/{nonce}/{sending_chain_id}/{new_usd_limit}")
106 }
107 BridgeAction::AssetPriceUpdateAction(a) => {
108 let chain_id = (a.chain_id as u8).to_string();
109 let nonce = a.nonce.to_string();
110 let token_id = a.token_id.to_string();
111 let new_usd_price = a.new_usd_price.to_string();
112 format!("sign/update_asset_price/{chain_id}/{nonce}/{token_id}/{new_usd_price}")
113 }
114 BridgeAction::EvmContractUpgradeAction(a) => {
115 let chain_id = (a.chain_id as u8).to_string();
116 let nonce = a.nonce.to_string();
117 let proxy_address = Hex::encode(a.proxy_address.as_slice());
118 let new_impl_address = Hex::encode(a.new_impl_address.as_slice());
119 let path = format!(
120 "sign/upgrade_evm_contract/{chain_id}/{nonce}/{proxy_address}/{new_impl_address}"
121 );
122 if a.call_data.is_empty() {
123 path
124 } else {
125 let call_data = Hex::encode(a.call_data.clone());
126 format!("{}/{}", path, call_data)
127 }
128 }
129 BridgeAction::AddTokensOnSuiAction(a) => {
130 let chain_id = (a.chain_id as u8).to_string();
131 let nonce = a.nonce.to_string();
132 let native = if a.native { "1" } else { "0" };
133 let token_ids = a
134 .token_ids
135 .iter()
136 .map(|id| id.to_string())
137 .collect::<Vec<_>>()
138 .join(",");
139 let token_type_names = a
140 .token_type_names
141 .iter()
142 .map(|name| name.to_canonical_string(true))
143 .collect::<Vec<_>>()
144 .join(",");
145 let token_prices = a
146 .token_prices
147 .iter()
148 .map(|price| price.to_string())
149 .collect::<Vec<_>>()
150 .join(",");
151 format!(
152 "sign/add_tokens_on_sui/{chain_id}/{nonce}/{native}/{token_ids}/{token_type_names}/{token_prices}"
153 )
154 }
155 BridgeAction::AddTokensOnEvmAction(a) => {
156 let chain_id = (a.chain_id as u8).to_string();
157 let nonce = a.nonce.to_string();
158 let native = if a.native { "1" } else { "0" };
159 let token_ids = a
160 .token_ids
161 .iter()
162 .map(|id| id.to_string())
163 .collect::<Vec<_>>()
164 .join(",");
165 let token_addresses = a
166 .token_addresses
167 .iter()
168 .map(|name| format!("{:?}", name))
169 .collect::<Vec<_>>()
170 .join(",");
171 let token_sui_decimals = a
172 .token_sui_decimals
173 .iter()
174 .map(|id| id.to_string())
175 .collect::<Vec<_>>()
176 .join(",");
177 let token_prices = a
178 .token_prices
179 .iter()
180 .map(|price| price.to_string())
181 .collect::<Vec<_>>()
182 .join(",");
183 format!(
184 "sign/add_tokens_on_evm/{chain_id}/{nonce}/{native}/{token_ids}/{token_addresses}/{token_sui_decimals}/{token_prices}"
185 )
186 }
187 }
188 }
189
190 pub async fn ping(&self) -> BridgeResult<bool> {
192 if self.base_url.is_none() {
193 return Err(BridgeError::InvalidAuthorityUrl(self.authority.clone()));
194 }
195 let url = self.base_url.clone().unwrap();
197 Ok(self
198 .inner
199 .get(url)
200 .header(reqwest::header::ACCEPT, APPLICATION_JSON)
201 .send()
202 .await?
203 .error_for_status()
204 .is_ok())
205 }
206
207 pub async fn request_sign_bridge_action(
208 &self,
209 action: BridgeAction,
210 ) -> BridgeResult<VerifiedSignedBridgeAction> {
211 if self.base_url.is_none() {
212 return Err(BridgeError::InvalidAuthorityUrl(self.authority.clone()));
213 }
214 let url = self
216 .base_url
217 .clone()
218 .unwrap()
219 .join(&Self::bridge_action_to_path(&action))?;
220 let resp = self
221 .inner
222 .get(url)
223 .header(reqwest::header::ACCEPT, APPLICATION_JSON)
224 .send()
225 .await?;
226 if !resp.status().is_success() {
227 let error_status = format!("{:?}", resp.error_for_status_ref());
228 let resp_text = String::from_utf8_lossy(
229 &read_limited_response_bytes(resp, MAX_BRIDGE_CLIENT_RESPONSE_SIZE).await?,
230 )
231 .to_string();
232 return match resp_text {
233 text if text.contains(&format!("{:?}", BridgeError::TxNotFinalized)) => {
234 Err(BridgeError::TxNotFinalized)
235 }
236 _ => Err(BridgeError::RestAPIError(format!(
237 "request_sign_bridge_action failed with status {:?}: {:?}",
238 error_status, resp_text
239 ))),
240 };
241 }
242 let signed_bridge_action: SignedBridgeAction = serde_json::from_slice(
243 &read_limited_response_bytes(resp, MAX_BRIDGE_CLIENT_RESPONSE_SIZE).await?,
244 )
245 .map_err(|e| {
246 BridgeError::RestAPIError(format!("Failed to deserialize bridge action response: {e}"))
247 })?;
248 verify_signed_bridge_action(
249 &action,
250 signed_bridge_action,
251 &self.authority,
252 &self.committee,
253 )
254 }
255}
256
257async fn read_limited_response_bytes(
258 mut response: reqwest::Response,
259 max_size: usize,
260) -> BridgeResult<Vec<u8>> {
261 if let Some(content_length) = response.content_length()
262 && content_length as usize > max_size
263 {
264 return Err(BridgeError::RestAPIError(format!(
265 "Response too large: {} bytes (max: {})",
266 content_length, max_size
267 )));
268 }
269
270 let mut bytes = Vec::new();
271 while let Some(chunk) = response.chunk().await? {
272 if bytes.len() + chunk.len() > max_size {
273 return Err(BridgeError::RestAPIError(format!(
274 "Response too large: exceeded {} bytes",
275 max_size
276 )));
277 }
278 bytes.extend_from_slice(&chunk);
279 }
280
281 Ok(bytes)
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287 use crate::test_utils::run_mock_bridge_server;
288 use crate::{
289 abi::EthToSuiTokenBridgeV1,
290 crypto::BridgeAuthoritySignInfo,
291 events::EmittedSuiToEthTokenBridgeV1,
292 server::mock_handler::BridgeRequestMockHandler,
293 test_utils::{get_test_authority_and_key, get_test_sui_to_eth_bridge_action},
294 types::SignedBridgeAction,
295 };
296 use alloy::primitives::{Address as EthAddress, TxHash};
297 use alloy::sol_types::SolValue;
298 use fastcrypto::hash::{HashFunction, Keccak256};
299 use fastcrypto::traits::KeyPair;
300 use prometheus::Registry;
301 use sui_types::TypeTag;
302 use sui_types::bridge::{BridgeChainId, TOKEN_ID_BTC, TOKEN_ID_USDT};
303 use sui_types::{base_types::SuiAddress, crypto::get_key_pair, digests::TransactionDigest};
304
305 #[tokio::test]
306 async fn test_bridge_client() {
307 telemetry_subscribers::init_for_testing();
308
309 let (mut authority, pubkey, _) = get_test_authority_and_key(10000, 12345);
310
311 let pubkey_bytes = BridgeAuthorityPublicKeyBytes::from(&pubkey);
312 let committee = Arc::new(BridgeCommittee::new(vec![authority.clone()]).unwrap());
313 let action =
314 get_test_sui_to_eth_bridge_action(None, Some(1), Some(1), Some(100), None, None, None);
315
316 let client = BridgeClient::new(pubkey_bytes.clone(), committee).unwrap();
318 assert!(client.base_url.is_some());
319
320 authority.base_url = "https://foo.suibridge.io".to_string();
322 let committee = Arc::new(BridgeCommittee::new(vec![authority.clone()]).unwrap());
323 let client = BridgeClient::new(pubkey_bytes.clone(), committee.clone()).unwrap();
324 assert!(client.base_url.is_some());
325
326 let (_, kp2): (_, fastcrypto::secp256k1::Secp256k1KeyPair) = get_key_pair();
328 let pubkey2_bytes = BridgeAuthorityPublicKeyBytes::from(kp2.public());
329 let err = BridgeClient::new(pubkey2_bytes, committee.clone()).unwrap_err();
330 assert!(matches!(err, BridgeError::InvalidBridgeAuthority(_)));
331
332 authority.base_url = "127.0.0.1:12345".to_string(); let committee = Arc::new(BridgeCommittee::new(vec![authority.clone()]).unwrap());
335 let client = BridgeClient::new(pubkey_bytes.clone(), committee.clone()).unwrap();
336 assert!(client.base_url.is_none());
337 assert!(matches!(
338 client.ping().await.unwrap_err(),
339 BridgeError::InvalidAuthorityUrl(_)
340 ));
341 assert!(matches!(
342 client
343 .request_sign_bridge_action(action.clone())
344 .await
345 .unwrap_err(),
346 BridgeError::InvalidAuthorityUrl(_)
347 ));
348
349 authority.base_url = "http://127.256.0.1:12345".to_string(); let committee = Arc::new(BridgeCommittee::new(vec![authority.clone()]).unwrap());
352 let client = BridgeClient::new(pubkey_bytes, committee.clone()).unwrap();
353 assert!(client.base_url.is_none());
354 assert!(matches!(
355 client.ping().await.unwrap_err(),
356 BridgeError::InvalidAuthorityUrl(_)
357 ));
358 assert!(matches!(
359 client
360 .request_sign_bridge_action(action.clone())
361 .await
362 .unwrap_err(),
363 BridgeError::InvalidAuthorityUrl(_)
364 ));
365 }
366
367 #[tokio::test]
368 async fn test_bridge_client_request_sign_action() {
369 telemetry_subscribers::init_for_testing();
370 let registry = Registry::new();
371 mysten_metrics::init_metrics(®istry);
372
373 let mock_handler = BridgeRequestMockHandler::new();
374
375 let (_handles, ports) = run_mock_bridge_server(vec![mock_handler.clone()]);
377
378 let port = ports[0];
379
380 let (authority, _pubkey, secret) = get_test_authority_and_key(5000, port);
381 let (authority2, _pubkey2, secret2) = get_test_authority_and_key(5000, port - 1);
382
383 let committee = BridgeCommittee::new(vec![authority.clone(), authority2.clone()]).unwrap();
384
385 let mut client =
386 BridgeClient::new(authority.pubkey_bytes(), Arc::new(committee.clone())).unwrap();
387
388 let tx_digest = TransactionDigest::random();
389 let event_idx = 4;
390
391 let action = get_test_sui_to_eth_bridge_action(
392 Some(tx_digest),
393 Some(event_idx),
394 Some(1),
395 Some(100),
396 None,
397 None,
398 None,
399 );
400 let sig = BridgeAuthoritySignInfo::new(&action, &secret);
401 let signed_event = SignedBridgeAction::new_from_data_and_sig(action.clone(), sig.clone());
402 mock_handler.add_sui_event_response(tx_digest, event_idx, Ok(signed_event.clone()), None);
403
404 client
406 .request_sign_bridge_action(action.clone())
407 .await
408 .unwrap();
409
410 let action2 = get_test_sui_to_eth_bridge_action(
412 Some(tx_digest),
413 Some(event_idx),
414 Some(2),
415 Some(200),
416 None,
417 None,
418 None,
419 );
420 let wrong_sig = BridgeAuthoritySignInfo::new(&action2, &secret);
421 let wrong_signed_action =
422 SignedBridgeAction::new_from_data_and_sig(action2.clone(), wrong_sig.clone());
423 mock_handler.add_sui_event_response(tx_digest, event_idx, Ok(wrong_signed_action), None);
424 let err = client
425 .request_sign_bridge_action(action.clone())
426 .await
427 .unwrap_err();
428 assert!(matches!(err, BridgeError::MismatchedAction));
429
430 let wrong_signed_action =
432 SignedBridgeAction::new_from_data_and_sig(action.clone(), wrong_sig);
433 mock_handler.add_sui_event_response(tx_digest, event_idx, Ok(wrong_signed_action), None);
434 let err = client
435 .request_sign_bridge_action(action.clone())
436 .await
437 .unwrap_err();
438 assert!(matches!(
439 err,
440 BridgeError::InvalidBridgeAuthoritySignature(..)
441 ));
442
443 let mut authority_blocklisted = authority.clone();
445 authority_blocklisted.is_blocklisted = true;
446 let committee2 = Arc::new(
447 BridgeCommittee::new(vec![authority_blocklisted.clone(), authority2.clone()]).unwrap(),
448 );
449 client.update_committee(committee2);
450 mock_handler.add_sui_event_response(tx_digest, event_idx, Ok(signed_event), None);
451
452 let err = client
453 .request_sign_bridge_action(action.clone())
454 .await
455 .unwrap_err();
456 assert!(
457 matches!(err, BridgeError::InvalidBridgeAuthority(pk) if pk == authority_blocklisted.pubkey_bytes()),
458 );
459
460 client.update_committee(committee.into());
461
462 let sig2 = BridgeAuthoritySignInfo::new(&action, &secret2);
464 let signed_event2 = SignedBridgeAction::new_from_data_and_sig(action.clone(), sig2.clone());
465 mock_handler.add_sui_event_response(tx_digest, event_idx, Ok(signed_event2), None);
466 let err = client
467 .request_sign_bridge_action(action.clone())
468 .await
469 .unwrap_err();
470 assert!(matches!(err, BridgeError::MismatchedAuthoritySigner));
471
472 let (_, kp3): (_, fastcrypto::secp256k1::Secp256k1KeyPair) = get_key_pair();
474 let secret3 = Arc::pin(kp3);
475 let sig3 = BridgeAuthoritySignInfo::new(&action, &secret3);
476 let signed_event3 = SignedBridgeAction::new_from_data_and_sig(action.clone(), sig3);
477 mock_handler.add_sui_event_response(tx_digest, event_idx, Ok(signed_event3), None);
478 let err = client
479 .request_sign_bridge_action(action.clone())
480 .await
481 .unwrap_err();
482 assert!(matches!(err, BridgeError::MismatchedAuthoritySigner));
483 }
484
485 #[test]
486 fn test_bridge_action_path_regression_tests() {
487 let sui_tx_digest = TransactionDigest::random();
488 let sui_tx_event_index = 5;
489 let action = BridgeAction::SuiToEthBridgeAction(crate::types::SuiToEthBridgeAction {
490 sui_tx_digest,
491 sui_tx_event_index,
492 sui_bridge_event: EmittedSuiToEthTokenBridgeV1 {
493 sui_chain_id: BridgeChainId::SuiCustom,
494 nonce: 1,
495 sui_address: SuiAddress::random_for_testing_only(),
496 eth_chain_id: BridgeChainId::EthSepolia,
497 eth_address: EthAddress::random(),
498 token_id: TOKEN_ID_USDT,
499 amount_sui_adjusted: 1,
500 },
501 });
502 assert_eq!(
503 BridgeClient::bridge_action_to_path(&action),
504 format!(
505 "sign/bridge_tx/sui/eth/{}/{}",
506 sui_tx_digest, sui_tx_event_index
507 )
508 );
509
510 let eth_tx_hash = TxHash::random();
511 let eth_event_index = 6;
512 let action = BridgeAction::EthToSuiBridgeAction(crate::types::EthToSuiBridgeAction {
513 eth_tx_hash,
514 eth_event_index,
515 eth_bridge_event: EthToSuiTokenBridgeV1 {
516 eth_chain_id: BridgeChainId::EthSepolia,
517 nonce: 1,
518 eth_address: EthAddress::random(),
519 sui_chain_id: BridgeChainId::SuiCustom,
520 sui_address: SuiAddress::random_for_testing_only(),
521 token_id: TOKEN_ID_USDT,
522 sui_adjusted_amount: 1,
523 },
524 });
525
526 assert_eq!(
527 BridgeClient::bridge_action_to_path(&action),
528 format!(
529 "sign/bridge_tx/eth/sui/{}/{}",
530 Hex::encode(eth_tx_hash.0),
531 eth_event_index
532 )
533 );
534
535 let pub_key_bytes = BridgeAuthorityPublicKeyBytes::from_bytes(
536 &Hex::decode("027f1178ff417fc9f5b8290bd8876f0a157a505a6c52db100a8492203ddd1d4279")
537 .unwrap(),
538 )
539 .unwrap();
540
541 let action =
542 BridgeAction::BlocklistCommitteeAction(crate::types::BlocklistCommitteeAction {
543 chain_id: BridgeChainId::EthSepolia,
544 nonce: 1,
545 blocklist_type: crate::types::BlocklistType::Blocklist,
546 members_to_update: vec![pub_key_bytes.clone()],
547 });
548 assert_eq!(
549 BridgeClient::bridge_action_to_path(&action),
550 "sign/update_committee_blocklist/11/1/0/027f1178ff417fc9f5b8290bd8876f0a157a505a6c52db100a8492203ddd1d4279",
551 );
552 let pub_key_bytes2 = BridgeAuthorityPublicKeyBytes::from_bytes(
553 &Hex::decode("02321ede33d2c2d7a8a152f275a1484edef2098f034121a602cb7d767d38680aa4")
554 .unwrap(),
555 )
556 .unwrap();
557 let action =
558 BridgeAction::BlocklistCommitteeAction(crate::types::BlocklistCommitteeAction {
559 chain_id: BridgeChainId::EthSepolia,
560 nonce: 1,
561 blocklist_type: crate::types::BlocklistType::Blocklist,
562 members_to_update: vec![pub_key_bytes.clone(), pub_key_bytes2.clone()],
563 });
564 assert_eq!(
565 BridgeClient::bridge_action_to_path(&action),
566 "sign/update_committee_blocklist/11/1/0/027f1178ff417fc9f5b8290bd8876f0a157a505a6c52db100a8492203ddd1d4279,02321ede33d2c2d7a8a152f275a1484edef2098f034121a602cb7d767d38680aa4",
567 );
568
569 let action = BridgeAction::EmergencyAction(crate::types::EmergencyAction {
570 chain_id: BridgeChainId::SuiCustom,
571 nonce: 5,
572 action_type: crate::types::EmergencyActionType::Pause,
573 });
574 assert_eq!(
575 BridgeClient::bridge_action_to_path(&action),
576 "sign/emergency_button/2/5/0",
577 );
578
579 let action = BridgeAction::LimitUpdateAction(crate::types::LimitUpdateAction {
580 chain_id: BridgeChainId::SuiCustom,
581 nonce: 10,
582 sending_chain_id: BridgeChainId::EthCustom,
583 new_usd_limit: 100,
584 });
585 assert_eq!(
586 BridgeClient::bridge_action_to_path(&action),
587 "sign/update_limit/2/10/12/100",
588 );
589
590 let action = BridgeAction::AssetPriceUpdateAction(crate::types::AssetPriceUpdateAction {
591 chain_id: BridgeChainId::SuiCustom,
592 nonce: 8,
593 token_id: TOKEN_ID_BTC,
594 new_usd_price: 100_000_000,
595 });
596 assert_eq!(
597 BridgeClient::bridge_action_to_path(&action),
598 "sign/update_asset_price/2/8/1/100000000",
599 );
600
601 let action =
602 BridgeAction::EvmContractUpgradeAction(crate::types::EvmContractUpgradeAction {
603 nonce: 123,
604 chain_id: BridgeChainId::EthCustom,
605 proxy_address: EthAddress::repeat_byte(6),
606 new_impl_address: EthAddress::repeat_byte(9),
607 call_data: vec![],
608 });
609 assert_eq!(
610 BridgeClient::bridge_action_to_path(&action),
611 "sign/upgrade_evm_contract/12/123/0606060606060606060606060606060606060606/0909090909090909090909090909090909090909",
612 );
613
614 let function_signature = "initializeV2()";
615 let selector = &Keccak256::digest(function_signature).digest[0..4];
616 let mut call_data = selector.to_vec();
617 let action =
618 BridgeAction::EvmContractUpgradeAction(crate::types::EvmContractUpgradeAction {
619 nonce: 123,
620 chain_id: BridgeChainId::EthCustom,
621 proxy_address: EthAddress::repeat_byte(6),
622 new_impl_address: EthAddress::repeat_byte(9),
623 call_data: call_data.clone(),
624 });
625 assert_eq!(
626 BridgeClient::bridge_action_to_path(&action),
627 "sign/upgrade_evm_contract/12/123/0606060606060606060606060606060606060606/0909090909090909090909090909090909090909/5cd8a76b",
628 );
629
630 call_data.extend(alloy::primitives::U256::from(42).abi_encode());
631 let action =
632 BridgeAction::EvmContractUpgradeAction(crate::types::EvmContractUpgradeAction {
633 nonce: 123,
634 chain_id: BridgeChainId::EthCustom,
635 proxy_address: EthAddress::repeat_byte(6),
636 new_impl_address: EthAddress::repeat_byte(9),
637 call_data,
638 });
639 assert_eq!(
640 BridgeClient::bridge_action_to_path(&action),
641 "sign/upgrade_evm_contract/12/123/0606060606060606060606060606060606060606/0909090909090909090909090909090909090909/5cd8a76b000000000000000000000000000000000000000000000000000000000000002a",
642 );
643
644 let action = BridgeAction::AddTokensOnSuiAction(crate::types::AddTokensOnSuiAction {
645 nonce: 3,
646 chain_id: BridgeChainId::SuiCustom,
647 native: false,
648 token_ids: vec![99, 100, 101],
649 token_type_names: vec![
650 TypeTag::from_str("0x0000000000000000000000000000000000000000000000000000000000000abc::my_coin::MyCoin1").unwrap(),
651 TypeTag::from_str("0x0000000000000000000000000000000000000000000000000000000000000abc::my_coin::MyCoin2").unwrap(),
652 TypeTag::from_str("0x0000000000000000000000000000000000000000000000000000000000000abc::my_coin::MyCoin3").unwrap(),
653 ],
654 token_prices: vec![1_000_000_000, 2_000_000_000, 3_000_000_000],
655 });
656 assert_eq!(
657 BridgeClient::bridge_action_to_path(&action),
658 "sign/add_tokens_on_sui/2/3/0/99,100,101/0x0000000000000000000000000000000000000000000000000000000000000abc::my_coin::MyCoin1,0x0000000000000000000000000000000000000000000000000000000000000abc::my_coin::MyCoin2,0x0000000000000000000000000000000000000000000000000000000000000abc::my_coin::MyCoin3/1000000000,2000000000,3000000000",
659 );
660
661 let action = BridgeAction::AddTokensOnEvmAction(crate::types::AddTokensOnEvmAction {
662 nonce: 0,
663 chain_id: BridgeChainId::EthCustom,
664 native: true,
665 token_ids: vec![99, 100, 101],
666 token_addresses: vec![
667 EthAddress::repeat_byte(1),
668 EthAddress::repeat_byte(2),
669 EthAddress::repeat_byte(3),
670 ],
671 token_sui_decimals: vec![5, 6, 7],
672 token_prices: vec![1_000_000_000, 2_000_000_000, 3_000_000_000],
673 });
674 assert_eq!(
675 BridgeClient::bridge_action_to_path(&action),
676 "sign/add_tokens_on_evm/12/0/1/99,100,101/0x0101010101010101010101010101010101010101,0x0202020202020202020202020202020202020202,0x0303030303030303030303030303030303030303/5,6,7/1000000000,2000000000,3000000000",
677 );
678 }
679
680 #[tokio::test]
681 async fn test_read_limited_response_bytes_rejects_oversized_response() {
682 let app = axum::Router::new().route(
683 "/large",
684 axum::routing::get(|| async { "a".repeat(MAX_BRIDGE_CLIENT_RESPONSE_SIZE + 1) }),
685 );
686
687 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
688 let addr = listener.local_addr().unwrap();
689 let server = tokio::spawn(async move {
690 axum::serve(listener, app).await.unwrap();
691 });
692
693 let response = reqwest::Client::new()
694 .get(format!("http://{addr}/large"))
695 .send()
696 .await
697 .unwrap();
698
699 let err = read_limited_response_bytes(response, MAX_BRIDGE_CLIENT_RESPONSE_SIZE)
700 .await
701 .unwrap_err();
702 assert!(matches!(err, BridgeError::RestAPIError(_)));
703
704 server.abort();
705 }
706}