fuels_contract/
contract_calls_utils.rs

1use fuel_gql_client::fuel_tx::{
2    field::Script as ScriptField, ConsensusParameters, Input, Output, TxPointer, UtxoId,
3};
4use fuel_gql_client::fuel_types::{bytes::padded_len_usize, Immediate18, Word};
5use fuel_gql_client::fuel_vm::{consts::REG_ONE, prelude::Opcode};
6use fuel_tx::{AssetId, Bytes32, ContractId};
7use fuels_core::constants::BASE_ASSET_ID;
8use fuels_types::bech32::Bech32Address;
9use fuels_types::constants::WORD_SIZE;
10use fuels_types::resource::Resource;
11use itertools::{chain, Itertools};
12use std::collections::HashSet;
13use std::{iter, vec};
14
15use crate::contract::ContractCall;
16
17#[derive(Default)]
18/// Specifies offsets of [`Opcode::CALL`] parameters stored in the script
19/// data from which they can be loaded into registers
20pub(crate) struct CallOpcodeParamsOffset {
21    pub asset_id_offset: usize,
22    pub amount_offset: usize,
23    pub gas_forwarded_offset: usize,
24    pub call_data_offset: usize,
25}
26
27/// Compute how much of each asset is required based on all `CallParameters` of the `ContractCalls`
28pub(crate) fn calculate_required_asset_amounts(calls: &[ContractCall]) -> Vec<(AssetId, u64)> {
29    let amounts_per_asset_id = calls
30        .iter()
31        .map(|call| (call.call_parameters.asset_id, call.call_parameters.amount))
32        .collect::<Vec<_>>();
33    sum_up_amounts_for_each_asset_id(amounts_per_asset_id)
34}
35
36/// Sum up the amounts required in each call for each asset ID, so you can get a total for each
37/// asset over all calls.
38fn sum_up_amounts_for_each_asset_id(
39    amounts_per_asset_id: Vec<(AssetId, u64)>,
40) -> Vec<(AssetId, u64)> {
41    amounts_per_asset_id
42        .into_iter()
43        .sorted_by_key(|(asset_id, _)| *asset_id)
44        .group_by(|(asset_id, _)| *asset_id)
45        .into_iter()
46        .map(|(asset_id, groups_w_same_asset_id)| {
47            let total_amount_in_group = groups_w_same_asset_id.map(|(_, amount)| amount).sum();
48            (asset_id, total_amount_in_group)
49        })
50        .collect()
51}
52
53/// Given a list of contract calls, create the actual opcodes used to call the contract
54pub(crate) fn get_instructions(
55    calls: &[ContractCall],
56    offsets: Vec<CallOpcodeParamsOffset>,
57) -> Vec<u8> {
58    let num_calls = calls.len();
59
60    let mut instructions = vec![];
61    for (_, call_offsets) in (0..num_calls).zip(offsets.iter()) {
62        instructions.extend(get_single_call_instructions(call_offsets));
63    }
64
65    instructions.extend(Opcode::RET(REG_ONE).to_bytes());
66
67    instructions
68}
69
70/// Returns script data, consisting of the following items in the given order:
71/// 1. Asset ID to be forwarded ([`AssetId::LEN`])
72/// 2. Amount to be forwarded `(1 * `[`WORD_SIZE`]`)`
73/// 3. Gas to be forwarded `(1 * `[`WORD_SIZE`]`)`
74/// 4. Contract ID ([`ContractId::LEN`]);
75/// 5. Function selector `(1 * `[`WORD_SIZE`]`)`
76/// 6. Calldata offset (optional) `(1 * `[`WORD_SIZE`]`)`
77/// 7. Encoded arguments (optional) (variable length)
78pub(crate) fn build_script_data_from_contract_calls(
79    calls: &[ContractCall],
80    data_offset: usize,
81    gas_limit: u64,
82) -> (Vec<u8>, Vec<CallOpcodeParamsOffset>) {
83    let mut script_data = vec![];
84    let mut param_offsets = vec![];
85
86    // The data for each call is ordered into segments
87    let mut segment_offset = data_offset;
88
89    for call in calls {
90        let call_param_offsets = CallOpcodeParamsOffset {
91            asset_id_offset: segment_offset,
92            amount_offset: segment_offset + AssetId::LEN,
93            gas_forwarded_offset: segment_offset + AssetId::LEN + WORD_SIZE,
94            call_data_offset: segment_offset + AssetId::LEN + 2 * WORD_SIZE,
95        };
96        param_offsets.push(call_param_offsets);
97
98        script_data.extend(call.call_parameters.asset_id.to_vec());
99
100        script_data.extend(call.call_parameters.amount.to_be_bytes());
101
102        // If gas_forwarded is not set, use the transaction gas limit
103        let gas_forwarded = call.call_parameters.gas_forwarded.unwrap_or(gas_limit);
104        script_data.extend(gas_forwarded.to_be_bytes());
105
106        script_data.extend(call.contract_id.hash().as_ref());
107
108        script_data.extend(call.encoded_selector);
109
110        // If the method call takes custom inputs or has more than
111        // one argument, we need to calculate the `call_data_offset`,
112        // which points to where the data for the custom types start in the
113        // transaction. If it doesn't take any custom inputs, this isn't necessary.
114        let encoded_args_start_offset = if call.compute_custom_input_offset {
115            // Custom inputs are stored after the previously added parameters,
116            // including custom_input_offset
117            let custom_input_offset =
118                segment_offset + AssetId::LEN + 2 * WORD_SIZE + ContractId::LEN + 2 * WORD_SIZE;
119            script_data.extend((custom_input_offset as Word).to_be_bytes());
120            custom_input_offset
121        } else {
122            segment_offset
123        };
124
125        let bytes = call.encoded_args.resolve(encoded_args_start_offset as u64);
126        script_data.extend(bytes);
127
128        // the data segment that holds the parameters for the next call
129        // begins at the original offset + the data we added so far
130        segment_offset = data_offset + script_data.len();
131    }
132
133    (script_data, param_offsets)
134}
135
136/// Returns the VM instructions for calling a contract method
137/// We use the [`Opcode`] to call a contract: [`CALL`](Opcode::CALL)
138/// pointing at the following registers:
139///
140/// 0x10 Script data offset
141/// 0x11 Gas forwarded
142/// 0x12 Coin amount
143/// 0x13 Asset ID
144///
145/// Note that these are soft rules as we're picking this addresses simply because they
146/// non-reserved register.
147fn get_single_call_instructions(offsets: &CallOpcodeParamsOffset) -> Vec<u8> {
148    let instructions = vec![
149        Opcode::MOVI(0x10, offsets.call_data_offset as Immediate18),
150        Opcode::MOVI(0x11, offsets.gas_forwarded_offset as Immediate18),
151        Opcode::LW(0x11, 0x11, 0),
152        Opcode::MOVI(0x12, offsets.amount_offset as Immediate18),
153        Opcode::LW(0x12, 0x12, 0),
154        Opcode::MOVI(0x13, offsets.asset_id_offset as Immediate18),
155        Opcode::CALL(0x10, 0x12, 0x13, 0x11),
156    ];
157
158    #[allow(clippy::iter_cloned_collect)]
159    instructions.iter().copied().collect::<Vec<u8>>()
160}
161
162/// Returns the assets and contracts that will be consumed ([`Input`]s)
163/// and created ([`Output`]s) by the transaction
164pub(crate) fn get_transaction_inputs_outputs(
165    calls: &[ContractCall],
166    wallet_address: &Bech32Address,
167    spendable_resources: Vec<Resource>,
168) -> (Vec<Input>, Vec<Output>) {
169    let asset_ids = extract_unique_asset_ids(&spendable_resources);
170    let contract_ids = extract_unique_contract_ids(calls);
171    let num_of_contracts = contract_ids.len();
172
173    let inputs = chain!(
174        generate_contract_inputs(contract_ids),
175        convert_to_signed_resources(spendable_resources),
176    )
177    .collect();
178
179    // Note the contract_outputs need to come first since the
180    // contract_inputs are referencing them via `output_index`. The node
181    // will, upon receiving our request, use `output_index` to index the
182    // `inputs` array we've sent over.
183    let outputs = chain!(
184        generate_contract_outputs(num_of_contracts),
185        generate_asset_change_outputs(wallet_address, asset_ids),
186        extract_variable_outputs(calls),
187        extract_message_outputs(calls)
188    )
189    .collect();
190    (inputs, outputs)
191}
192
193fn extract_unique_asset_ids(spendable_coins: &[Resource]) -> HashSet<AssetId> {
194    spendable_coins
195        .iter()
196        .map(|resource| match resource {
197            Resource::Coin(coin) => coin.asset_id,
198            Resource::Message(_) => BASE_ASSET_ID,
199        })
200        .collect()
201}
202
203fn extract_variable_outputs(calls: &[ContractCall]) -> Vec<Output> {
204    calls
205        .iter()
206        .filter_map(|call| call.variable_outputs.clone())
207        .flatten()
208        .collect()
209}
210
211fn extract_message_outputs(calls: &[ContractCall]) -> Vec<Output> {
212    calls
213        .iter()
214        .filter_map(|call| call.message_outputs.clone())
215        .flatten()
216        .collect()
217}
218
219fn generate_asset_change_outputs(
220    wallet_address: &Bech32Address,
221    asset_ids: HashSet<AssetId>,
222) -> Vec<Output> {
223    asset_ids
224        .into_iter()
225        .map(|asset_id| Output::change(wallet_address.into(), 0, asset_id))
226        .collect()
227}
228
229fn generate_contract_outputs(num_of_contracts: usize) -> Vec<Output> {
230    (0..num_of_contracts)
231        .map(|idx| Output::contract(idx as u8, Bytes32::zeroed(), Bytes32::zeroed()))
232        .collect()
233}
234
235fn convert_to_signed_resources(spendable_resources: Vec<Resource>) -> Vec<Input> {
236    spendable_resources
237        .into_iter()
238        .map(|resource| match resource {
239            Resource::Coin(coin) => Input::coin_signed(
240                coin.utxo_id,
241                coin.owner.into(),
242                coin.amount,
243                coin.asset_id,
244                TxPointer::default(),
245                0,
246                coin.maturity,
247            ),
248            Resource::Message(message) => Input::message_signed(
249                message.message_id(),
250                message.sender.into(),
251                message.recipient.into(),
252                message.amount,
253                message.nonce,
254                0,
255                message.data,
256            ),
257        })
258        .collect()
259}
260
261/// Gets the base offset for a script. The offset depends on the `max_inputs`
262/// field of the `ConsensusParameters` and the static offset
263pub fn get_base_script_offset(consensus_parameters: &ConsensusParameters) -> usize {
264    consensus_parameters.tx_offset() + fuel_tx::Script::script_offset_static()
265}
266
267/// Calculates the length of the script based on the number of contract calls it
268/// has to make and returns the offset at which the script data begins
269pub(crate) fn get_data_offset(
270    consensus_parameters: &ConsensusParameters,
271    num_calls: usize,
272) -> usize {
273    // use placeholder for call param offsets, we only care about the length
274    let len_script =
275        get_single_call_instructions(&CallOpcodeParamsOffset::default()).len() * num_calls;
276
277    // Opcode::LEN is a placeholder for the RET instruction which is added later
278    let opcode_len = Opcode::LEN;
279
280    get_base_script_offset(consensus_parameters) + padded_len_usize(len_script + opcode_len)
281}
282
283fn generate_contract_inputs(contract_ids: HashSet<ContractId>) -> Vec<Input> {
284    contract_ids
285        .into_iter()
286        .enumerate()
287        .map(|(idx, contract_id)| {
288            Input::contract(
289                UtxoId::new(Bytes32::zeroed(), idx as u8),
290                Bytes32::zeroed(),
291                Bytes32::zeroed(),
292                TxPointer::default(),
293                contract_id,
294            )
295        })
296        .collect()
297}
298
299fn extract_unique_contract_ids(calls: &[ContractCall]) -> HashSet<ContractId> {
300    calls
301        .iter()
302        .flat_map(|call| {
303            call.external_contracts
304                .iter()
305                .map(|bech32| bech32.into())
306                .chain(iter::once((&call.contract_id).into()))
307        })
308        .collect()
309}
310
311#[cfg(test)]
312mod test {
313    use super::*;
314    use fuels_core::abi_encoder::ABIEncoder;
315    use fuels_core::parameters::CallParameters;
316    use fuels_core::Token;
317    use fuels_types::bech32::Bech32ContractId;
318    use fuels_types::coin::{Coin, CoinStatus};
319    use fuels_types::param_types::ParamType;
320    use rand::Rng;
321    use std::slice;
322
323    impl ContractCall {
324        pub fn new_with_random_id() -> Self {
325            ContractCall {
326                contract_id: random_bech32_contract_id(),
327                encoded_args: Default::default(),
328                encoded_selector: [0; 8],
329                call_parameters: Default::default(),
330                compute_custom_input_offset: false,
331                variable_outputs: None,
332                external_contracts: Default::default(),
333                output_param: ParamType::Unit,
334                message_outputs: None,
335            }
336        }
337    }
338
339    fn random_bech32_addr() -> Bech32Address {
340        Bech32Address::new("fuel", rand::thread_rng().gen::<[u8; 32]>())
341    }
342
343    fn random_bech32_contract_id() -> Bech32ContractId {
344        Bech32ContractId::new("fuel", rand::thread_rng().gen::<[u8; 32]>())
345    }
346
347    #[tokio::test]
348    async fn test_script_data() {
349        // Arrange
350        const SELECTOR_LEN: usize = WORD_SIZE;
351        const NUM_CALLS: usize = 3;
352
353        let contract_ids = vec![
354            Bech32ContractId::new("test", Bytes32::new([1u8; 32])),
355            Bech32ContractId::new("test", Bytes32::new([1u8; 32])),
356            Bech32ContractId::new("test", Bytes32::new([1u8; 32])),
357        ];
358
359        let asset_ids = vec![
360            AssetId::from([4u8; 32]),
361            AssetId::from([5u8; 32]),
362            AssetId::from([6u8; 32]),
363        ];
364
365        let selectors = vec![[7u8; 8], [8u8; 8], [9u8; 8]];
366
367        // Call 2 has multiple inputs, compute_custom_input_offset will be true
368
369        let args = [Token::U8(1), Token::U16(2), Token::U8(3)]
370            .map(|token| ABIEncoder::encode(&[token]).unwrap())
371            .to_vec();
372
373        let calls: Vec<ContractCall> = (0..NUM_CALLS)
374            .map(|i| ContractCall {
375                contract_id: contract_ids[i].clone(),
376                encoded_selector: selectors[i],
377                encoded_args: args[i].clone(),
378                call_parameters: CallParameters::new(
379                    Some(i as u64),
380                    Some(asset_ids[i]),
381                    Some(i as u64),
382                ),
383                compute_custom_input_offset: i == 1,
384                variable_outputs: None,
385                message_outputs: None,
386                external_contracts: vec![],
387                output_param: ParamType::Unit,
388            })
389            .collect();
390
391        // Act
392        let (script_data, param_offsets) = build_script_data_from_contract_calls(&calls, 0, 0);
393
394        // Assert
395        assert_eq!(param_offsets.len(), NUM_CALLS);
396        for (idx, offsets) in param_offsets.iter().enumerate() {
397            let asset_id = script_data
398                [offsets.asset_id_offset..offsets.asset_id_offset + AssetId::LEN]
399                .to_vec();
400            assert_eq!(asset_id, asset_ids[idx].to_vec());
401
402            let amount =
403                script_data[offsets.amount_offset..offsets.amount_offset + WORD_SIZE].to_vec();
404            assert_eq!(amount, idx.to_be_bytes());
405
406            let gas = script_data
407                [offsets.gas_forwarded_offset..offsets.gas_forwarded_offset + WORD_SIZE]
408                .to_vec();
409            assert_eq!(gas, idx.to_be_bytes().to_vec());
410
411            let contract_id =
412                &script_data[offsets.call_data_offset..offsets.call_data_offset + ContractId::LEN];
413            let expected_contract_id = contract_ids[idx].hash();
414            assert_eq!(contract_id, expected_contract_id.as_slice());
415
416            let selector_offset = offsets.call_data_offset + ContractId::LEN;
417            let selector = script_data[selector_offset..selector_offset + SELECTOR_LEN].to_vec();
418            assert_eq!(selector, selectors[idx].to_vec());
419        }
420
421        // Calls 1 and 3 have their input arguments after the selector
422        let call_1_arg_offset = param_offsets[0].call_data_offset + ContractId::LEN + SELECTOR_LEN;
423        let call_1_arg = script_data[call_1_arg_offset..call_1_arg_offset + WORD_SIZE].to_vec();
424        assert_eq!(call_1_arg, args[0].resolve(0));
425
426        let call_3_arg_offset = param_offsets[2].call_data_offset + ContractId::LEN + SELECTOR_LEN;
427        let call_3_arg = script_data[call_3_arg_offset..call_3_arg_offset + WORD_SIZE].to_vec();
428        assert_eq!(call_3_arg, args[2].resolve(0));
429
430        // Call 2 has custom inputs and custom_input_offset
431        let call_2_arg_offset = param_offsets[1].call_data_offset + ContractId::LEN + SELECTOR_LEN;
432        let custom_input_offset =
433            script_data[call_2_arg_offset..call_2_arg_offset + WORD_SIZE].to_vec();
434        assert_eq!(
435            custom_input_offset,
436            (call_2_arg_offset + WORD_SIZE).to_be_bytes()
437        );
438
439        let custom_input_offset =
440            param_offsets[1].call_data_offset + ContractId::LEN + SELECTOR_LEN + WORD_SIZE;
441        let custom_input =
442            script_data[custom_input_offset..custom_input_offset + WORD_SIZE].to_vec();
443        assert_eq!(custom_input, args[1].resolve(0));
444    }
445
446    #[test]
447    fn contract_input_present() {
448        let call = ContractCall::new_with_random_id();
449
450        let (inputs, _) = get_transaction_inputs_outputs(
451            slice::from_ref(&call),
452            &random_bech32_addr(),
453            Default::default(),
454        );
455
456        assert_eq!(
457            inputs,
458            vec![Input::contract(
459                UtxoId::new(Bytes32::zeroed(), 0),
460                Bytes32::zeroed(),
461                Bytes32::zeroed(),
462                TxPointer::default(),
463                call.contract_id.into(),
464            )]
465        );
466    }
467
468    #[test]
469    fn contract_input_is_not_duplicated() {
470        let call = ContractCall::new_with_random_id();
471        let call_w_same_contract =
472            ContractCall::new_with_random_id().with_contract_id(call.contract_id.clone());
473
474        let calls = [call, call_w_same_contract];
475
476        let (inputs, _) =
477            get_transaction_inputs_outputs(&calls, &random_bech32_addr(), Default::default());
478
479        assert_eq!(
480            inputs,
481            vec![Input::contract(
482                UtxoId::new(Bytes32::zeroed(), 0),
483                Bytes32::zeroed(),
484                Bytes32::zeroed(),
485                TxPointer::default(),
486                calls[0].contract_id.clone().into(),
487            )]
488        );
489    }
490
491    #[test]
492    fn contract_output_present() {
493        let call = ContractCall::new_with_random_id();
494
495        let (_, outputs) =
496            get_transaction_inputs_outputs(&[call], &random_bech32_addr(), Default::default());
497
498        assert_eq!(
499            outputs,
500            vec![Output::contract(0, Bytes32::zeroed(), Bytes32::zeroed())]
501        );
502    }
503
504    #[test]
505    fn external_contract_input_present() {
506        // given
507        let external_contract_id = random_bech32_contract_id();
508        let call = ContractCall::new_with_random_id()
509            .with_external_contracts(vec![external_contract_id.clone()]);
510
511        // when
512        let (inputs, _) = get_transaction_inputs_outputs(
513            slice::from_ref(&call),
514            &random_bech32_addr(),
515            Default::default(),
516        );
517
518        // then
519        let mut expected_contract_ids: HashSet<ContractId> =
520            [call.contract_id.into(), external_contract_id.into()].into();
521
522        for (index, input) in inputs.into_iter().enumerate() {
523            match input {
524                Input::Contract {
525                    utxo_id,
526                    balance_root,
527                    state_root,
528                    tx_pointer,
529                    contract_id,
530                } => {
531                    assert_eq!(utxo_id, UtxoId::new(Bytes32::zeroed(), index as u8));
532                    assert_eq!(balance_root, Bytes32::zeroed());
533                    assert_eq!(state_root, Bytes32::zeroed());
534                    assert_eq!(tx_pointer, TxPointer::default());
535                    assert!(expected_contract_ids.contains(&contract_id));
536                    expected_contract_ids.remove(&contract_id);
537                }
538                _ => {
539                    panic!("Expected only inputs of type Input::Contract");
540                }
541            }
542        }
543    }
544
545    #[test]
546    fn external_contract_output_present() {
547        // given
548        let external_contract_id = random_bech32_contract_id();
549        let call =
550            ContractCall::new_with_random_id().with_external_contracts(vec![external_contract_id]);
551
552        // when
553        let (_, outputs) =
554            get_transaction_inputs_outputs(&[call], &random_bech32_addr(), Default::default());
555
556        // then
557        let expected_outputs = (0..=1)
558            .map(|i| Output::contract(i, Bytes32::zeroed(), Bytes32::zeroed()))
559            .collect::<Vec<_>>();
560
561        assert_eq!(outputs, expected_outputs);
562    }
563
564    #[test]
565    fn change_per_asset_id_added() {
566        // given
567        let wallet_addr = random_bech32_addr();
568        let asset_ids = [AssetId::default(), AssetId::from([1; 32])];
569
570        let coins = asset_ids
571            .into_iter()
572            .map(|asset_id| {
573                Resource::Coin(Coin {
574                    amount: 100,
575                    block_created: 0,
576                    asset_id,
577                    utxo_id: Default::default(),
578                    maturity: 0,
579                    owner: Default::default(),
580                    status: CoinStatus::Unspent,
581                })
582            })
583            .collect();
584        let call = ContractCall::new_with_random_id();
585
586        // when
587        let (_, outputs) = get_transaction_inputs_outputs(&[call], &wallet_addr, coins);
588
589        // then
590        let change_outputs: HashSet<Output> = outputs[1..].iter().cloned().collect();
591
592        let expected_change_outputs = asset_ids
593            .into_iter()
594            .map(|asset_id| Output::Change {
595                to: wallet_addr.clone().into(),
596                amount: 0,
597                asset_id,
598            })
599            .collect();
600
601        assert_eq!(change_outputs, expected_change_outputs);
602    }
603
604    #[test]
605    fn spendable_coins_added_to_input() {
606        // given
607        let asset_ids = [AssetId::default(), AssetId::from([1; 32])];
608
609        let generate_spendable_resources = || {
610            asset_ids
611                .into_iter()
612                .enumerate()
613                .map(|(index, asset_id)| {
614                    Resource::Coin(Coin {
615                        amount: (index * 10) as u64,
616                        block_created: 1,
617                        asset_id,
618                        utxo_id: Default::default(),
619                        maturity: 0,
620                        owner: Default::default(),
621                        status: CoinStatus::Unspent,
622                    })
623                })
624                .collect::<Vec<_>>()
625        };
626
627        let call = ContractCall::new_with_random_id();
628
629        // when
630        let (inputs, _) = get_transaction_inputs_outputs(
631            &[call],
632            &random_bech32_addr(),
633            generate_spendable_resources(),
634        );
635
636        // then
637        let inputs_as_signed_coins: HashSet<Input> = inputs[1..].iter().cloned().collect();
638
639        let expected_inputs = generate_spendable_resources()
640            .into_iter()
641            .map(|resource| match resource {
642                Resource::Coin(coin) => Input::coin_signed(
643                    coin.utxo_id,
644                    coin.owner.into(),
645                    coin.amount,
646                    coin.asset_id,
647                    TxPointer::default(),
648                    0,
649                    0,
650                ),
651                Resource::Message(_) => panic!("Resources contained messages."),
652            })
653            .collect::<HashSet<_>>();
654
655        assert_eq!(expected_inputs, inputs_as_signed_coins);
656    }
657
658    #[test]
659    fn variable_outputs_appended_to_outputs() {
660        // given
661        let variable_outputs = [100, 200].map(|amount| {
662            Output::variable(random_bech32_addr().into(), amount, Default::default())
663        });
664
665        let calls = variable_outputs
666            .iter()
667            .cloned()
668            .map(|variable_output| {
669                ContractCall::new_with_random_id().with_variable_outputs(vec![variable_output])
670            })
671            .collect::<Vec<_>>();
672
673        // when
674        let (_, outputs) =
675            get_transaction_inputs_outputs(&calls, &random_bech32_addr(), Default::default());
676
677        // then
678        let actual_variable_outputs: HashSet<Output> = outputs[2..].iter().cloned().collect();
679        let expected_outputs: HashSet<Output> = variable_outputs.into();
680
681        assert_eq!(expected_outputs, actual_variable_outputs);
682    }
683
684    #[test]
685    fn message_outputs_appended_to_outputs() {
686        // given
687        let message_outputs =
688            [100, 200].map(|amount| Output::message(random_bech32_addr().into(), amount));
689
690        let calls = message_outputs
691            .iter()
692            .cloned()
693            .map(|message_output| {
694                ContractCall::new_with_random_id().with_message_outputs(vec![message_output])
695            })
696            .collect::<Vec<_>>();
697
698        // when
699        let (_, outputs) =
700            get_transaction_inputs_outputs(&calls, &random_bech32_addr(), Default::default());
701
702        // then
703        let actual_message_outputs: HashSet<Output> = outputs[2..].iter().cloned().collect();
704        let expected_outputs: HashSet<Output> = message_outputs.into();
705
706        assert_eq!(expected_outputs, actual_message_outputs);
707    }
708
709    #[test]
710    fn will_collate_same_asset_ids() {
711        let asset_id_1 = AssetId::from([1; 32]);
712        let asset_id_2 = AssetId::from([2; 32]);
713
714        let calls = [
715            (asset_id_1, 100),
716            (asset_id_2, 200),
717            (asset_id_1, 300),
718            (asset_id_2, 400),
719        ]
720        .map(|(asset_id, amount)| CallParameters::new(Some(amount), Some(asset_id), None))
721        .map(|call_parameters| {
722            ContractCall::new_with_random_id().with_call_parameters(call_parameters)
723        });
724
725        let asset_id_amounts = calculate_required_asset_amounts(&calls);
726
727        let expected_asset_id_amounts = [(asset_id_1, 400), (asset_id_2, 600)].into();
728
729        assert_eq!(
730            asset_id_amounts.into_iter().collect::<HashSet<_>>(),
731            expected_asset_id_amounts
732        )
733    }
734}