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