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