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, 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// Note: `base_url` is `Option<Url>` because `quorum_map_then_reduce_with_timeout_and_prefs`
19// uses `[]` to get Client based on key. Therefore even when the URL is invalid we need to
20// create a Client instance.
21// TODO: In the future we can consider change `quorum_map_then_reduce_with_timeout_and_prefs`
22// and its callsites to use `get` instead of `[]`.
23#[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        // Unwrap safe: we passed the `is_active_member` check above
40        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    // Important: the paths need to match the ones in server/mod.rs
58    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    // Returns Ok(true) if the server is up and running
191    pub async fn ping(&self) -> BridgeResult<bool> {
192        if self.base_url.is_none() {
193            return Err(BridgeError::InvalidAuthorityUrl(self.authority.clone()));
194        }
195        // Unwrap safe: checked `self.base_url.is_none()` above
196        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        // Unwrap safe: checked `self.base_url.is_none()` above
215        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        // Ok
317        let client = BridgeClient::new(pubkey_bytes.clone(), committee).unwrap();
318        assert!(client.base_url.is_some());
319
320        // Ok
321        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        // Err, not in committee
327        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        // invalid base url
333        authority.base_url = "127.0.0.1:12345".to_string(); // <-- bad, missing http://
334        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        // invalid base url
350        authority.base_url = "http://127.256.0.1:12345".to_string(); // <-- bad, invalid ipv4 address
351        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(&registry);
372
373        let mock_handler = BridgeRequestMockHandler::new();
374
375        // start server
376        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        // success
405        client
406            .request_sign_bridge_action(action.clone())
407            .await
408            .unwrap();
409
410        // mismatched action would fail, this could happen when the authority fetched the wrong event
411        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        // The action matches but the signature is wrong, fail
431        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        // sig from blocklisted authority would fail
444        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        // signed by a different authority in committee would fail
463        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        // signed by a different key, not in committee, would fail
473        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}