sui_bridge/client/
bridge_client.rs

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