sui_bridge/
sui_transaction_builder.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use fastcrypto::traits::ToFromBytes;
5use move_core_types::ident_str;
6use std::{collections::HashMap, str::FromStr};
7use sui_types::bridge::{
8    BRIDGE_CREATE_ADD_TOKEN_ON_SUI_MESSAGE_FUNCTION_NAME,
9    BRIDGE_EXECUTE_SYSTEM_MESSAGE_FUNCTION_NAME, BRIDGE_MESSAGE_MODULE_NAME, BRIDGE_MODULE_NAME,
10};
11use sui_types::transaction::CallArg;
12use sui_types::{BRIDGE_PACKAGE_ID, Identifier};
13use sui_types::{
14    TypeTag,
15    base_types::{ObjectRef, SuiAddress},
16    programmable_transaction_builder::ProgrammableTransactionBuilder,
17    transaction::{ObjectArg, TransactionData},
18};
19
20use crate::{
21    error::{BridgeError, BridgeResult},
22    types::{BridgeAction, VerifiedCertifiedBridgeAction},
23};
24
25pub fn build_sui_transaction(
26    client_address: SuiAddress,
27    gas_object_ref: &ObjectRef,
28    action: VerifiedCertifiedBridgeAction,
29    bridge_object_arg: ObjectArg,
30    sui_token_type_tags: &HashMap<u8, TypeTag>,
31    rgp: u64,
32) -> BridgeResult<TransactionData> {
33    // TODO: Check chain id?
34    match action.data() {
35        BridgeAction::EthToSuiBridgeAction(_) => build_token_bridge_approve_transaction(
36            client_address,
37            gas_object_ref,
38            action,
39            true,
40            bridge_object_arg,
41            sui_token_type_tags,
42            rgp,
43        ),
44        BridgeAction::SuiToEthBridgeAction(_) => build_token_bridge_approve_transaction(
45            client_address,
46            gas_object_ref,
47            action,
48            false,
49            bridge_object_arg,
50            sui_token_type_tags,
51            rgp,
52        ),
53        BridgeAction::SuiToEthTokenTransfer(_) => build_token_bridge_approve_transaction(
54            client_address,
55            gas_object_ref,
56            action,
57            false,
58            bridge_object_arg,
59            sui_token_type_tags,
60            rgp,
61        ),
62        BridgeAction::BlocklistCommitteeAction(_) => build_committee_blocklist_approve_transaction(
63            client_address,
64            gas_object_ref,
65            action,
66            bridge_object_arg,
67            rgp,
68        ),
69        BridgeAction::EmergencyAction(_) => build_emergency_op_approve_transaction(
70            client_address,
71            gas_object_ref,
72            action,
73            bridge_object_arg,
74            rgp,
75        ),
76        BridgeAction::LimitUpdateAction(_) => build_limit_update_approve_transaction(
77            client_address,
78            gas_object_ref,
79            action,
80            bridge_object_arg,
81            rgp,
82        ),
83        BridgeAction::AssetPriceUpdateAction(_) => build_asset_price_update_approve_transaction(
84            client_address,
85            gas_object_ref,
86            action,
87            bridge_object_arg,
88            rgp,
89        ),
90        BridgeAction::EvmContractUpgradeAction(_) => {
91            // It does not need a Sui tranaction to execute EVM contract upgrade
92            unreachable!()
93        }
94        BridgeAction::AddTokensOnSuiAction(_) => build_add_tokens_on_sui_transaction(
95            client_address,
96            gas_object_ref,
97            action,
98            bridge_object_arg,
99            rgp,
100        ),
101        BridgeAction::AddTokensOnEvmAction(_) => {
102            // It does not need a Sui tranaction to add tokens on EVM
103            unreachable!()
104        }
105    }
106}
107
108fn build_token_bridge_approve_transaction(
109    client_address: SuiAddress,
110    gas_object_ref: &ObjectRef,
111    action: VerifiedCertifiedBridgeAction,
112    claim: bool,
113    bridge_object_arg: ObjectArg,
114    sui_token_type_tags: &HashMap<u8, TypeTag>,
115    rgp: u64,
116) -> BridgeResult<TransactionData> {
117    let (bridge_action, sigs) = action.into_inner().into_data_and_sig();
118    let mut builder = ProgrammableTransactionBuilder::new();
119
120    let (source_chain, seq_num, sender, target_chain, target, token_type, amount) =
121        match bridge_action {
122            BridgeAction::SuiToEthBridgeAction(a) => {
123                let bridge_event = a.sui_bridge_event;
124                (
125                    bridge_event.sui_chain_id,
126                    bridge_event.nonce,
127                    bridge_event.sui_address.to_vec(),
128                    bridge_event.eth_chain_id,
129                    bridge_event.eth_address.to_fixed_bytes().to_vec(),
130                    bridge_event.token_id,
131                    bridge_event.amount_sui_adjusted,
132                )
133            }
134            BridgeAction::SuiToEthTokenTransfer(a) => (
135                a.sui_chain_id,
136                a.nonce,
137                a.sui_address.to_vec(),
138                a.eth_chain_id,
139                a.eth_address.to_fixed_bytes().to_vec(),
140                a.token_id,
141                a.amount_adjusted,
142            ),
143            BridgeAction::EthToSuiBridgeAction(a) => {
144                let bridge_event = a.eth_bridge_event;
145                (
146                    bridge_event.eth_chain_id,
147                    bridge_event.nonce,
148                    bridge_event.eth_address.to_fixed_bytes().to_vec(),
149                    bridge_event.sui_chain_id,
150                    bridge_event.sui_address.to_vec(),
151                    bridge_event.token_id,
152                    bridge_event.sui_adjusted_amount,
153                )
154            }
155            _ => unreachable!(),
156        };
157
158    let source_chain = builder.pure(source_chain as u8).unwrap();
159    let seq_num = builder.pure(seq_num).unwrap();
160    let sender = builder.pure(sender.clone()).map_err(|e| {
161        BridgeError::BridgeSerializationError(format!(
162            "Failed to serialize sender: {:?}. Err: {:?}",
163            sender, e
164        ))
165    })?;
166    let target_chain = builder.pure(target_chain as u8).unwrap();
167    let target = builder.pure(target.clone()).map_err(|e| {
168        BridgeError::BridgeSerializationError(format!(
169            "Failed to serialize target: {:?}. Err: {:?}",
170            target, e
171        ))
172    })?;
173    let arg_token_type = builder.pure(token_type).unwrap();
174    let amount = builder.pure(amount).unwrap();
175
176    let arg_msg = builder.programmable_move_call(
177        BRIDGE_PACKAGE_ID,
178        ident_str!("message").to_owned(),
179        ident_str!("create_token_bridge_message").to_owned(),
180        vec![],
181        vec![
182            source_chain,
183            seq_num,
184            sender,
185            target_chain,
186            target,
187            arg_token_type,
188            amount,
189        ],
190    );
191
192    // Unwrap: these should not fail
193    let arg_bridge = builder.obj(bridge_object_arg).unwrap();
194    let arg_clock = builder.input(CallArg::CLOCK_IMM).unwrap();
195
196    let mut sig_bytes = vec![];
197    for (_, sig) in sigs.signatures {
198        sig_bytes.push(sig.as_bytes().to_vec());
199    }
200    let arg_signatures = builder.pure(sig_bytes.clone()).map_err(|e| {
201        BridgeError::BridgeSerializationError(format!(
202            "Failed to serialize signatures: {:?}. Err: {:?}",
203            sig_bytes, e
204        ))
205    })?;
206
207    builder.programmable_move_call(
208        BRIDGE_PACKAGE_ID,
209        sui_types::bridge::BRIDGE_MODULE_NAME.to_owned(),
210        ident_str!("approve_token_transfer").to_owned(),
211        vec![],
212        vec![arg_bridge, arg_msg, arg_signatures],
213    );
214
215    if claim {
216        builder.programmable_move_call(
217            BRIDGE_PACKAGE_ID,
218            sui_types::bridge::BRIDGE_MODULE_NAME.to_owned(),
219            ident_str!("claim_and_transfer_token").to_owned(),
220            vec![
221                sui_token_type_tags
222                    .get(&token_type)
223                    .ok_or(BridgeError::UnknownTokenId(token_type))?
224                    .clone(),
225            ],
226            vec![arg_bridge, arg_clock, source_chain, seq_num],
227        );
228    }
229
230    let pt = builder.finish();
231
232    Ok(TransactionData::new_programmable(
233        client_address,
234        vec![*gas_object_ref],
235        pt,
236        100_000_000,
237        rgp,
238    ))
239}
240
241fn build_emergency_op_approve_transaction(
242    client_address: SuiAddress,
243    gas_object_ref: &ObjectRef,
244    action: VerifiedCertifiedBridgeAction,
245    bridge_object_arg: ObjectArg,
246    rgp: u64,
247) -> BridgeResult<TransactionData> {
248    let (bridge_action, sigs) = action.into_inner().into_data_and_sig();
249
250    let mut builder = ProgrammableTransactionBuilder::new();
251
252    let (source_chain, seq_num, action_type) = match bridge_action {
253        BridgeAction::EmergencyAction(a) => (a.chain_id, a.nonce, a.action_type),
254        _ => unreachable!(),
255    };
256
257    // Unwrap: these should not fail
258    let source_chain = builder.pure(source_chain as u8).unwrap();
259    let seq_num = builder.pure(seq_num).unwrap();
260    let action_type = builder.pure(action_type as u8).unwrap();
261    let arg_bridge = builder.obj(bridge_object_arg).unwrap();
262
263    let arg_msg = builder.programmable_move_call(
264        BRIDGE_PACKAGE_ID,
265        ident_str!("message").to_owned(),
266        ident_str!("create_emergency_op_message").to_owned(),
267        vec![],
268        vec![source_chain, seq_num, action_type],
269    );
270
271    let mut sig_bytes = vec![];
272    for (_, sig) in sigs.signatures {
273        sig_bytes.push(sig.as_bytes().to_vec());
274    }
275    let arg_signatures = builder.pure(sig_bytes.clone()).map_err(|e| {
276        BridgeError::BridgeSerializationError(format!(
277            "Failed to serialize signatures: {:?}. Err: {:?}",
278            sig_bytes, e
279        ))
280    })?;
281
282    builder.programmable_move_call(
283        BRIDGE_PACKAGE_ID,
284        ident_str!("bridge").to_owned(),
285        ident_str!("execute_system_message").to_owned(),
286        vec![],
287        vec![arg_bridge, arg_msg, arg_signatures],
288    );
289
290    let pt = builder.finish();
291
292    Ok(TransactionData::new_programmable(
293        client_address,
294        vec![*gas_object_ref],
295        pt,
296        100_000_000,
297        rgp,
298    ))
299}
300
301fn build_committee_blocklist_approve_transaction(
302    client_address: SuiAddress,
303    gas_object_ref: &ObjectRef,
304    action: VerifiedCertifiedBridgeAction,
305    bridge_object_arg: ObjectArg,
306    rgp: u64,
307) -> BridgeResult<TransactionData> {
308    let (bridge_action, sigs) = action.into_inner().into_data_and_sig();
309
310    let mut builder = ProgrammableTransactionBuilder::new();
311
312    let (source_chain, seq_num, blocklist_type, members_to_update) = match bridge_action {
313        BridgeAction::BlocklistCommitteeAction(a) => {
314            (a.chain_id, a.nonce, a.blocklist_type, a.members_to_update)
315        }
316        _ => unreachable!(),
317    };
318
319    // Unwrap: these should not fail
320    let source_chain = builder.pure(source_chain as u8).unwrap();
321    let seq_num = builder.pure(seq_num).unwrap();
322    let blocklist_type = builder.pure(blocklist_type as u8).unwrap();
323    let members_to_update = members_to_update
324        .into_iter()
325        .map(|m| m.to_eth_address().as_bytes().to_vec())
326        .collect::<Vec<_>>();
327    let members_to_update = builder.pure(members_to_update).unwrap();
328    let arg_bridge = builder.obj(bridge_object_arg).unwrap();
329
330    let arg_msg = builder.programmable_move_call(
331        BRIDGE_PACKAGE_ID,
332        ident_str!("message").to_owned(),
333        ident_str!("create_blocklist_message").to_owned(),
334        vec![],
335        vec![source_chain, seq_num, blocklist_type, members_to_update],
336    );
337
338    let mut sig_bytes = vec![];
339    for (_, sig) in sigs.signatures {
340        sig_bytes.push(sig.as_bytes().to_vec());
341    }
342    let arg_signatures = builder.pure(sig_bytes.clone()).map_err(|e| {
343        BridgeError::BridgeSerializationError(format!(
344            "Failed to serialize signatures: {:?}. Err: {:?}",
345            sig_bytes, e
346        ))
347    })?;
348
349    builder.programmable_move_call(
350        BRIDGE_PACKAGE_ID,
351        ident_str!("bridge").to_owned(),
352        ident_str!("execute_system_message").to_owned(),
353        vec![],
354        vec![arg_bridge, arg_msg, arg_signatures],
355    );
356
357    let pt = builder.finish();
358
359    Ok(TransactionData::new_programmable(
360        client_address,
361        vec![*gas_object_ref],
362        pt,
363        100_000_000,
364        rgp,
365    ))
366}
367
368fn build_limit_update_approve_transaction(
369    client_address: SuiAddress,
370    gas_object_ref: &ObjectRef,
371    action: VerifiedCertifiedBridgeAction,
372    bridge_object_arg: ObjectArg,
373    rgp: u64,
374) -> BridgeResult<TransactionData> {
375    let (bridge_action, sigs) = action.into_inner().into_data_and_sig();
376
377    let mut builder = ProgrammableTransactionBuilder::new();
378
379    let (receiving_chain_id, seq_num, sending_chain_id, new_usd_limit) = match bridge_action {
380        BridgeAction::LimitUpdateAction(a) => {
381            (a.chain_id, a.nonce, a.sending_chain_id, a.new_usd_limit)
382        }
383        _ => unreachable!(),
384    };
385
386    // Unwrap: these should not fail
387    let receiving_chain_id = builder.pure(receiving_chain_id as u8).unwrap();
388    let seq_num = builder.pure(seq_num).unwrap();
389    let sending_chain_id = builder.pure(sending_chain_id as u8).unwrap();
390    let new_usd_limit = builder.pure(new_usd_limit).unwrap();
391    let arg_bridge = builder.obj(bridge_object_arg).unwrap();
392
393    let arg_msg = builder.programmable_move_call(
394        BRIDGE_PACKAGE_ID,
395        ident_str!("message").to_owned(),
396        ident_str!("create_update_bridge_limit_message").to_owned(),
397        vec![],
398        vec![receiving_chain_id, seq_num, sending_chain_id, new_usd_limit],
399    );
400
401    let mut sig_bytes = vec![];
402    for (_, sig) in sigs.signatures {
403        sig_bytes.push(sig.as_bytes().to_vec());
404    }
405    let arg_signatures = builder.pure(sig_bytes.clone()).map_err(|e| {
406        BridgeError::BridgeSerializationError(format!(
407            "Failed to serialize signatures: {:?}. Err: {:?}",
408            sig_bytes, e
409        ))
410    })?;
411
412    builder.programmable_move_call(
413        BRIDGE_PACKAGE_ID,
414        ident_str!("bridge").to_owned(),
415        ident_str!("execute_system_message").to_owned(),
416        vec![],
417        vec![arg_bridge, arg_msg, arg_signatures],
418    );
419
420    let pt = builder.finish();
421
422    Ok(TransactionData::new_programmable(
423        client_address,
424        vec![*gas_object_ref],
425        pt,
426        100_000_000,
427        rgp,
428    ))
429}
430
431fn build_asset_price_update_approve_transaction(
432    client_address: SuiAddress,
433    gas_object_ref: &ObjectRef,
434    action: VerifiedCertifiedBridgeAction,
435    bridge_object_arg: ObjectArg,
436    rgp: u64,
437) -> BridgeResult<TransactionData> {
438    let (bridge_action, sigs) = action.into_inner().into_data_and_sig();
439
440    let mut builder = ProgrammableTransactionBuilder::new();
441
442    let (source_chain, seq_num, token_id, new_usd_price) = match bridge_action {
443        BridgeAction::AssetPriceUpdateAction(a) => {
444            (a.chain_id, a.nonce, a.token_id, a.new_usd_price)
445        }
446        _ => unreachable!(),
447    };
448
449    // Unwrap: these should not fail
450    let source_chain = builder.pure(source_chain as u8).unwrap();
451    let token_id = builder.pure(token_id).unwrap();
452    let seq_num = builder.pure(seq_num).unwrap();
453    let new_price = builder.pure(new_usd_price).unwrap();
454    let arg_bridge = builder.obj(bridge_object_arg).unwrap();
455
456    let arg_msg = builder.programmable_move_call(
457        BRIDGE_PACKAGE_ID,
458        ident_str!("message").to_owned(),
459        ident_str!("create_update_asset_price_message").to_owned(),
460        vec![],
461        vec![token_id, source_chain, seq_num, new_price],
462    );
463
464    let mut sig_bytes = vec![];
465    for (_, sig) in sigs.signatures {
466        sig_bytes.push(sig.as_bytes().to_vec());
467    }
468    let arg_signatures = builder.pure(sig_bytes.clone()).map_err(|e| {
469        BridgeError::BridgeSerializationError(format!(
470            "Failed to serialize signatures: {:?}. Err: {:?}",
471            sig_bytes, e
472        ))
473    })?;
474
475    builder.programmable_move_call(
476        BRIDGE_PACKAGE_ID,
477        ident_str!("bridge").to_owned(),
478        ident_str!("execute_system_message").to_owned(),
479        vec![],
480        vec![arg_bridge, arg_msg, arg_signatures],
481    );
482
483    let pt = builder.finish();
484
485    Ok(TransactionData::new_programmable(
486        client_address,
487        vec![*gas_object_ref],
488        pt,
489        100_000_000,
490        rgp,
491    ))
492}
493
494pub fn build_add_tokens_on_sui_transaction(
495    client_address: SuiAddress,
496    gas_object_ref: &ObjectRef,
497    action: VerifiedCertifiedBridgeAction,
498    bridge_object_arg: ObjectArg,
499    rgp: u64,
500) -> BridgeResult<TransactionData> {
501    let (bridge_action, sigs) = action.into_inner().into_data_and_sig();
502
503    let mut builder = ProgrammableTransactionBuilder::new();
504
505    let (source_chain, seq_num, native, token_ids, token_type_names, token_prices) =
506        match bridge_action {
507            BridgeAction::AddTokensOnSuiAction(a) => (
508                a.chain_id,
509                a.nonce,
510                a.native,
511                a.token_ids,
512                a.token_type_names,
513                a.token_prices,
514            ),
515            _ => unreachable!(),
516        };
517    let token_type_names = token_type_names
518        .iter()
519        .map(|type_name| type_name.to_canonical_string(false))
520        .collect::<Vec<_>>();
521    let source_chain = builder.pure(source_chain as u8).unwrap();
522    let seq_num = builder.pure(seq_num).unwrap();
523    let native_token = builder.pure(native).unwrap();
524    let token_ids = builder.pure(token_ids).unwrap();
525    let token_type_names = builder.pure(token_type_names).unwrap();
526    let token_prices = builder.pure(token_prices).unwrap();
527
528    let message_arg = builder.programmable_move_call(
529        BRIDGE_PACKAGE_ID,
530        BRIDGE_MESSAGE_MODULE_NAME.into(),
531        BRIDGE_CREATE_ADD_TOKEN_ON_SUI_MESSAGE_FUNCTION_NAME.into(),
532        vec![],
533        vec![
534            source_chain,
535            seq_num,
536            native_token,
537            token_ids,
538            token_type_names,
539            token_prices,
540        ],
541    );
542
543    let bridge_arg = builder.obj(bridge_object_arg).unwrap();
544
545    let mut sig_bytes = vec![];
546    for (_, sig) in sigs.signatures {
547        sig_bytes.push(sig.as_bytes().to_vec());
548    }
549    let sigs_arg = builder.pure(sig_bytes.clone()).unwrap();
550
551    builder.programmable_move_call(
552        BRIDGE_PACKAGE_ID,
553        BRIDGE_MODULE_NAME.into(),
554        BRIDGE_EXECUTE_SYSTEM_MESSAGE_FUNCTION_NAME.into(),
555        vec![],
556        vec![bridge_arg, message_arg, sigs_arg],
557    );
558
559    let pt = builder.finish();
560
561    Ok(TransactionData::new_programmable(
562        client_address,
563        vec![*gas_object_ref],
564        pt,
565        100_000_000,
566        rgp,
567    ))
568}
569
570pub fn build_committee_register_transaction(
571    validator_address: SuiAddress,
572    gas_object_ref: &ObjectRef,
573    bridge_object_arg: ObjectArg,
574    bridge_authority_pub_key_bytes: Vec<u8>,
575    bridge_url: &str,
576    ref_gas_price: u64,
577    gas_budget: u64,
578) -> BridgeResult<TransactionData> {
579    let mut builder = ProgrammableTransactionBuilder::new();
580    let system_state = builder.obj(ObjectArg::SUI_SYSTEM_MUT).unwrap();
581    let bridge = builder.obj(bridge_object_arg).unwrap();
582    let bridge_pubkey = builder
583        .input(CallArg::Pure(
584            bcs::to_bytes(&bridge_authority_pub_key_bytes).unwrap(),
585        ))
586        .unwrap();
587    let url = builder
588        .input(CallArg::Pure(bcs::to_bytes(bridge_url.as_bytes()).unwrap()))
589        .unwrap();
590    builder.programmable_move_call(
591        BRIDGE_PACKAGE_ID,
592        BRIDGE_MODULE_NAME.into(),
593        Identifier::from_str("committee_registration").unwrap(),
594        vec![],
595        vec![bridge, system_state, bridge_pubkey, url],
596    );
597    let data = TransactionData::new_programmable(
598        validator_address,
599        vec![*gas_object_ref],
600        builder.finish(),
601        gas_budget,
602        ref_gas_price,
603    );
604    Ok(data)
605}
606
607pub fn build_committee_update_url_transaction(
608    validator_address: SuiAddress,
609    gas_object_ref: &ObjectRef,
610    bridge_object_arg: ObjectArg,
611    bridge_url: &str,
612    ref_gas_price: u64,
613    gas_budget: u64,
614) -> BridgeResult<TransactionData> {
615    let mut builder = ProgrammableTransactionBuilder::new();
616    let bridge = builder.obj(bridge_object_arg).unwrap();
617    let url = builder
618        .input(CallArg::Pure(bcs::to_bytes(bridge_url.as_bytes()).unwrap()))
619        .unwrap();
620    builder.programmable_move_call(
621        BRIDGE_PACKAGE_ID,
622        BRIDGE_MODULE_NAME.into(),
623        Identifier::from_str("update_node_url").unwrap(),
624        vec![],
625        vec![bridge, url],
626    );
627    let data = TransactionData::new_programmable(
628        validator_address,
629        vec![*gas_object_ref],
630        builder.finish(),
631        gas_budget,
632        ref_gas_price,
633    );
634    Ok(data)
635}
636
637#[cfg(test)]
638mod tests {
639    use crate::crypto::BridgeAuthorityKeyPair;
640    use crate::e2e_tests::test_utils::TestClusterWrapperBuilder;
641    use crate::metrics::BridgeMetrics;
642    use crate::sui_client::SuiClient;
643    use crate::types::BridgeAction;
644    use crate::types::EmergencyAction;
645    use crate::types::EmergencyActionType;
646    use crate::types::*;
647    use crate::{
648        crypto::BridgeAuthorityPublicKeyBytes,
649        test_utils::{
650            approve_action_with_validator_secrets, bridge_token, get_test_eth_to_sui_bridge_action,
651            get_test_sui_to_eth_bridge_action,
652        },
653    };
654    use ethers::types::Address as EthAddress;
655    use std::collections::HashMap;
656    use std::sync::Arc;
657    use sui_types::bridge::{BridgeChainId, TOKEN_ID_BTC, TOKEN_ID_USDC};
658    use sui_types::crypto::ToFromBytes;
659    use sui_types::crypto::get_key_pair;
660
661    #[tokio::test(flavor = "multi_thread", worker_threads = 8)]
662    async fn test_build_sui_transaction_for_token_transfer() {
663        telemetry_subscribers::init_for_testing();
664        let mut bridge_keys = vec![];
665        for _ in 0..=3 {
666            let (_, kp): (_, BridgeAuthorityKeyPair) = get_key_pair();
667            bridge_keys.push(kp);
668        }
669        let mut test_cluster = TestClusterWrapperBuilder::new()
670            .with_bridge_authority_keys(bridge_keys)
671            .with_deploy_tokens(true)
672            .build()
673            .await;
674
675        let metrics = Arc::new(BridgeMetrics::new_for_testing());
676        let sui_client = SuiClient::new(&test_cluster.inner.fullnode_handle.rpc_url, metrics)
677            .await
678            .unwrap();
679        let bridge_authority_keys = test_cluster.authority_keys_clone();
680
681        // Note: We don't call `sui_client.get_bridge_committee` here because it will err if the committee
682        // is not initialized during the construction of `BridgeCommittee`.
683        test_cluster
684            .trigger_reconfiguration_if_not_yet_and_assert_bridge_committee_initialized()
685            .await;
686        let context = &mut test_cluster.inner.wallet;
687        let sender = context.active_address().unwrap();
688        let usdc_amount = 5000000;
689        let bridge_object_arg = sui_client
690            .get_mutable_bridge_object_arg_must_succeed()
691            .await;
692        let id_token_map = sui_client.get_token_id_map().await.unwrap();
693
694        // 1. Test Eth -> Sui Transfer approval
695        let action = get_test_eth_to_sui_bridge_action(None, Some(usdc_amount), Some(sender), None);
696        // `approve_action_with_validator_secrets` covers transaction building
697        let usdc_object_ref = approve_action_with_validator_secrets(
698            context,
699            bridge_object_arg,
700            action.clone(),
701            &bridge_authority_keys,
702            Some(sender),
703            &id_token_map,
704        )
705        .await
706        .unwrap();
707
708        // 2. Test Sui -> Eth Transfer approval
709        let bridge_event = bridge_token(
710            context,
711            EthAddress::random(),
712            usdc_object_ref,
713            id_token_map.get(&TOKEN_ID_USDC).unwrap().clone(),
714            bridge_object_arg,
715        )
716        .await;
717
718        let action = get_test_sui_to_eth_bridge_action(
719            None,
720            None,
721            Some(bridge_event.nonce),
722            Some(bridge_event.amount_sui_adjusted),
723            Some(bridge_event.sui_address),
724            Some(bridge_event.eth_address),
725            Some(TOKEN_ID_USDC),
726        );
727        // `approve_action_with_validator_secrets` covers transaction building
728        approve_action_with_validator_secrets(
729            context,
730            bridge_object_arg,
731            action.clone(),
732            &bridge_authority_keys,
733            None,
734            &id_token_map,
735        )
736        .await;
737    }
738
739    #[tokio::test(flavor = "multi_thread", worker_threads = 8)]
740    async fn test_build_sui_transaction_for_emergency_op() {
741        telemetry_subscribers::init_for_testing();
742        let num_valdiator = 2;
743        let mut bridge_keys = vec![];
744        for _ in 0..num_valdiator {
745            let (_, kp): (_, BridgeAuthorityKeyPair) = get_key_pair();
746            bridge_keys.push(kp);
747        }
748        let mut test_cluster = TestClusterWrapperBuilder::new()
749            .with_bridge_authority_keys(bridge_keys)
750            .with_deploy_tokens(true)
751            .build()
752            .await;
753        let metrics = Arc::new(BridgeMetrics::new_for_testing());
754        let sui_client = SuiClient::new(&test_cluster.inner.fullnode_handle.rpc_url, metrics)
755            .await
756            .unwrap();
757        let bridge_authority_keys = test_cluster.authority_keys_clone();
758
759        // Wait until committee is set up
760        test_cluster
761            .trigger_reconfiguration_if_not_yet_and_assert_bridge_committee_initialized()
762            .await;
763        let summary = sui_client.get_bridge_summary().await.unwrap();
764        assert!(!summary.is_frozen);
765
766        let context = &mut test_cluster.inner.wallet;
767        let bridge_object_arg = sui_client
768            .get_mutable_bridge_object_arg_must_succeed()
769            .await;
770        let id_token_map = sui_client.get_token_id_map().await.unwrap();
771
772        // 1. Pause
773        let action = BridgeAction::EmergencyAction(EmergencyAction {
774            nonce: 0,
775            chain_id: BridgeChainId::SuiCustom,
776            action_type: EmergencyActionType::Pause,
777        });
778        // `approve_action_with_validator_secrets` covers transaction building
779        approve_action_with_validator_secrets(
780            context,
781            bridge_object_arg,
782            action.clone(),
783            &bridge_authority_keys,
784            None,
785            &id_token_map,
786        )
787        .await;
788        let summary = sui_client.get_bridge_summary().await.unwrap();
789        assert!(summary.is_frozen);
790
791        // 2. Unpause
792        let action = BridgeAction::EmergencyAction(EmergencyAction {
793            nonce: 1,
794            chain_id: BridgeChainId::SuiCustom,
795            action_type: EmergencyActionType::Unpause,
796        });
797        // `approve_action_with_validator_secrets` covers transaction building
798        approve_action_with_validator_secrets(
799            context,
800            bridge_object_arg,
801            action.clone(),
802            &bridge_authority_keys,
803            None,
804            &id_token_map,
805        )
806        .await;
807        let summary = sui_client.get_bridge_summary().await.unwrap();
808        assert!(!summary.is_frozen);
809    }
810
811    #[tokio::test(flavor = "multi_thread", worker_threads = 8)]
812    async fn test_build_sui_transaction_for_committee_blocklist() {
813        telemetry_subscribers::init_for_testing();
814        let mut bridge_keys = vec![];
815        for _ in 0..=3 {
816            let (_, kp): (_, BridgeAuthorityKeyPair) = get_key_pair();
817            bridge_keys.push(kp);
818        }
819        let mut test_cluster = TestClusterWrapperBuilder::new()
820            .with_bridge_authority_keys(bridge_keys)
821            .with_deploy_tokens(true)
822            .build()
823            .await;
824        let metrics = Arc::new(BridgeMetrics::new_for_testing());
825        let sui_client = SuiClient::new(&test_cluster.inner.fullnode_handle.rpc_url, metrics)
826            .await
827            .unwrap();
828        let bridge_authority_keys = test_cluster.authority_keys_clone();
829
830        // Wait until committee is set up
831        test_cluster
832            .trigger_reconfiguration_if_not_yet_and_assert_bridge_committee_initialized()
833            .await;
834        let committee = sui_client.get_bridge_summary().await.unwrap().committee;
835        let victim = committee.members.first().unwrap().clone().1;
836        for member in committee.members {
837            assert!(!member.1.blocklisted);
838        }
839
840        let context = &mut test_cluster.inner.wallet;
841        let bridge_object_arg = sui_client
842            .get_mutable_bridge_object_arg_must_succeed()
843            .await;
844        let id_token_map = sui_client.get_token_id_map().await.unwrap();
845
846        // 1. blocklist The victim
847        let action = BridgeAction::BlocklistCommitteeAction(BlocklistCommitteeAction {
848            nonce: 0,
849            chain_id: BridgeChainId::SuiCustom,
850            blocklist_type: BlocklistType::Blocklist,
851            members_to_update: vec![
852                BridgeAuthorityPublicKeyBytes::from_bytes(&victim.bridge_pubkey_bytes).unwrap(),
853            ],
854        });
855        // `approve_action_with_validator_secrets` covers transaction building
856        approve_action_with_validator_secrets(
857            context,
858            bridge_object_arg,
859            action.clone(),
860            &bridge_authority_keys,
861            None,
862            &id_token_map,
863        )
864        .await;
865        let committee = sui_client.get_bridge_summary().await.unwrap().committee;
866        for member in committee.members {
867            if member.1.bridge_pubkey_bytes == victim.bridge_pubkey_bytes {
868                assert!(member.1.blocklisted);
869            } else {
870                assert!(!member.1.blocklisted);
871            }
872        }
873
874        // 2. unblocklist the victim
875        let action = BridgeAction::BlocklistCommitteeAction(BlocklistCommitteeAction {
876            nonce: 1,
877            chain_id: BridgeChainId::SuiCustom,
878            blocklist_type: BlocklistType::Unblocklist,
879            members_to_update: vec![
880                BridgeAuthorityPublicKeyBytes::from_bytes(&victim.bridge_pubkey_bytes).unwrap(),
881            ],
882        });
883        // `approve_action_with_validator_secrets` covers transaction building
884        approve_action_with_validator_secrets(
885            context,
886            bridge_object_arg,
887            action.clone(),
888            &bridge_authority_keys,
889            None,
890            &id_token_map,
891        )
892        .await;
893        let committee = sui_client.get_bridge_summary().await.unwrap().committee;
894        for member in committee.members {
895            assert!(!member.1.blocklisted);
896        }
897    }
898
899    #[tokio::test(flavor = "multi_thread", worker_threads = 8)]
900    async fn test_build_sui_transaction_for_limit_update() {
901        telemetry_subscribers::init_for_testing();
902        let mut bridge_keys = vec![];
903        for _ in 0..=3 {
904            let (_, kp): (_, BridgeAuthorityKeyPair) = get_key_pair();
905            bridge_keys.push(kp);
906        }
907        let mut test_cluster = TestClusterWrapperBuilder::new()
908            .with_bridge_authority_keys(bridge_keys)
909            .with_deploy_tokens(true)
910            .build()
911            .await;
912        let metrics = Arc::new(BridgeMetrics::new_for_testing());
913        let sui_client = SuiClient::new(&test_cluster.inner.fullnode_handle.rpc_url, metrics)
914            .await
915            .unwrap();
916        let bridge_authority_keys = test_cluster.authority_keys_clone();
917
918        // Wait until committee is set up
919        test_cluster
920            .trigger_reconfiguration_if_not_yet_and_assert_bridge_committee_initialized()
921            .await;
922        let transfer_limit = sui_client
923            .get_bridge_summary()
924            .await
925            .unwrap()
926            .limiter
927            .transfer_limit
928            .into_iter()
929            .map(|(s, d, l)| ((s, d), l))
930            .collect::<HashMap<_, _>>();
931
932        let context = &mut test_cluster.inner.wallet;
933        let bridge_object_arg = sui_client
934            .get_mutable_bridge_object_arg_must_succeed()
935            .await;
936        let id_token_map = sui_client.get_token_id_map().await.unwrap();
937
938        // update limit
939        let action = BridgeAction::LimitUpdateAction(LimitUpdateAction {
940            nonce: 0,
941            chain_id: BridgeChainId::SuiCustom,
942            sending_chain_id: BridgeChainId::EthCustom,
943            new_usd_limit: 6_666_666 * USD_MULTIPLIER, // $1M USD
944        });
945        // `approve_action_with_validator_secrets` covers transaction building
946        approve_action_with_validator_secrets(
947            context,
948            bridge_object_arg,
949            action.clone(),
950            &bridge_authority_keys,
951            None,
952            &id_token_map,
953        )
954        .await;
955        let new_transfer_limit = sui_client
956            .get_bridge_summary()
957            .await
958            .unwrap()
959            .limiter
960            .transfer_limit;
961        for limit in new_transfer_limit {
962            if limit.0 == BridgeChainId::EthCustom && limit.1 == BridgeChainId::SuiCustom {
963                assert_eq!(limit.2, 6_666_666 * USD_MULTIPLIER);
964            } else {
965                assert_eq!(limit.2, *transfer_limit.get(&(limit.0, limit.1)).unwrap());
966            }
967        }
968    }
969
970    #[tokio::test(flavor = "multi_thread", worker_threads = 8)]
971    async fn test_build_sui_transaction_for_price_update() {
972        telemetry_subscribers::init_for_testing();
973        let mut bridge_keys = vec![];
974        for _ in 0..=3 {
975            let (_, kp): (_, BridgeAuthorityKeyPair) = get_key_pair();
976            bridge_keys.push(kp);
977        }
978        let mut test_cluster = TestClusterWrapperBuilder::new()
979            .with_bridge_authority_keys(bridge_keys)
980            .with_deploy_tokens(true)
981            .build()
982            .await;
983        let metrics = Arc::new(BridgeMetrics::new_for_testing());
984        let sui_client = SuiClient::new(&test_cluster.inner.fullnode_handle.rpc_url, metrics)
985            .await
986            .unwrap();
987        let bridge_authority_keys = test_cluster.authority_keys_clone();
988
989        // Note: We don't call `sui_client.get_bridge_committee` here because it will err if the committee
990        // is not initialized during the construction of `BridgeCommittee`.
991        test_cluster
992            .trigger_reconfiguration_if_not_yet_and_assert_bridge_committee_initialized()
993            .await;
994        let notional_values = sui_client.get_notional_values().await.unwrap();
995        assert_ne!(notional_values[&TOKEN_ID_USDC], 69_000 * USD_MULTIPLIER);
996
997        let context = &mut test_cluster.inner.wallet;
998        let bridge_object_arg = sui_client
999            .get_mutable_bridge_object_arg_must_succeed()
1000            .await;
1001        let id_token_map = sui_client.get_token_id_map().await.unwrap();
1002
1003        // update price
1004        let action = BridgeAction::AssetPriceUpdateAction(AssetPriceUpdateAction {
1005            nonce: 0,
1006            chain_id: BridgeChainId::SuiCustom,
1007            token_id: TOKEN_ID_BTC,
1008            new_usd_price: 69_000 * USD_MULTIPLIER, // $69k USD
1009        });
1010        // `approve_action_with_validator_secrets` covers transaction building
1011        approve_action_with_validator_secrets(
1012            context,
1013            bridge_object_arg,
1014            action.clone(),
1015            &bridge_authority_keys,
1016            None,
1017            &id_token_map,
1018        )
1019        .await;
1020        let new_notional_values = sui_client.get_notional_values().await.unwrap();
1021        for (token_id, price) in new_notional_values {
1022            if token_id == TOKEN_ID_BTC {
1023                assert_eq!(price, 69_000 * USD_MULTIPLIER);
1024            } else {
1025                assert_eq!(price, *notional_values.get(&token_id).unwrap());
1026            }
1027        }
1028    }
1029}