fuels_programs/calls/
utils.rs

1use std::{collections::HashSet, iter, vec};
2
3use fuel_abi_types::error_codes::FAILED_TRANSFER_TO_ADDRESS_SIGNAL;
4use fuel_asm::{op, RegId};
5use fuel_tx::{AssetId, Bytes32, ContractId, Output, PanicReason, Receipt, TxPointer, UtxoId};
6use fuels_accounts::Account;
7use fuels_core::{
8    offsets::call_script_data_offset,
9    types::{
10        bech32::{Bech32Address, Bech32ContractId},
11        errors::Result,
12        input::Input,
13        transaction::{ScriptTransaction, TxPolicies},
14        transaction_builders::{
15            BuildableTransaction, ScriptTransactionBuilder, TransactionBuilder,
16            VariableOutputPolicy,
17        },
18    },
19};
20use itertools::{chain, Itertools};
21
22use crate::{
23    assembly::contract_call::{CallOpcodeParamsOffset, ContractCallInstructions},
24    calls::ContractCall,
25    DEFAULT_MAX_FEE_ESTIMATION_TOLERANCE,
26};
27
28pub(crate) mod sealed {
29    pub trait Sealed {}
30}
31
32/// Creates a [`ScriptTransactionBuilder`] from contract calls.
33pub(crate) async fn transaction_builder_from_contract_calls(
34    calls: &[ContractCall],
35    tx_policies: TxPolicies,
36    variable_outputs: VariableOutputPolicy,
37    account: &impl Account,
38) -> Result<ScriptTransactionBuilder> {
39    let calls_instructions_len = compute_calls_instructions_len(calls);
40    let provider = account.try_provider()?;
41    let consensus_parameters = provider.consensus_parameters().await?;
42    let data_offset = call_script_data_offset(&consensus_parameters, calls_instructions_len)?;
43
44    let (script_data, call_param_offsets) = build_script_data_from_contract_calls(
45        calls,
46        data_offset,
47        *consensus_parameters.base_asset_id(),
48    )?;
49    let script = get_instructions(call_param_offsets);
50
51    let required_asset_amounts =
52        calculate_required_asset_amounts(calls, *consensus_parameters.base_asset_id());
53
54    // Find the spendable resources required for those calls
55    let mut asset_inputs = vec![];
56    for (asset_id, amount) in &required_asset_amounts {
57        let resources = account
58            .get_asset_inputs_for_amount(*asset_id, *amount, None)
59            .await?;
60        asset_inputs.extend(resources);
61    }
62
63    let (inputs, outputs) = get_transaction_inputs_outputs(
64        calls,
65        asset_inputs,
66        account.address(),
67        *consensus_parameters.base_asset_id(),
68    );
69
70    Ok(ScriptTransactionBuilder::default()
71        .with_variable_output_policy(variable_outputs)
72        .with_tx_policies(tx_policies)
73        .with_script(script)
74        .with_script_data(script_data.clone())
75        .with_inputs(inputs)
76        .with_outputs(outputs)
77        .with_gas_estimation_tolerance(DEFAULT_MAX_FEE_ESTIMATION_TOLERANCE)
78        .with_max_fee_estimation_tolerance(DEFAULT_MAX_FEE_ESTIMATION_TOLERANCE))
79}
80
81/// Creates a [`ScriptTransaction`] from contract calls. The internal [Transaction] is
82/// initialized with the actual script instructions, script data needed to perform the call and
83/// transaction inputs/outputs consisting of assets and contracts.
84pub(crate) async fn build_tx_from_contract_calls(
85    calls: &[ContractCall],
86    tx_policies: TxPolicies,
87    variable_outputs: VariableOutputPolicy,
88    account: &impl Account,
89) -> Result<ScriptTransaction> {
90    let mut tb =
91        transaction_builder_from_contract_calls(calls, tx_policies, variable_outputs, account)
92            .await?;
93
94    let consensus_parameters = account.try_provider()?.consensus_parameters().await?;
95    let base_asset_id = *consensus_parameters.base_asset_id();
96    let required_asset_amounts = calculate_required_asset_amounts(calls, base_asset_id);
97
98    let used_base_amount = required_asset_amounts
99        .iter()
100        .find_map(|(asset_id, amount)| (*asset_id == base_asset_id).then_some(*amount))
101        .unwrap_or_default();
102
103    account.add_witnesses(&mut tb)?;
104    account.adjust_for_fee(&mut tb, used_base_amount).await?;
105
106    tb.build(account.try_provider()?).await
107}
108
109/// Compute the length of the calling scripts for the two types of contract calls: those that return
110/// a heap type, and those that don't.
111fn compute_calls_instructions_len(calls: &[ContractCall]) -> usize {
112    calls
113        .iter()
114        .map(|c| {
115            // Use placeholder for `call_param_offsets` and `output_param_type`, because the length of
116            // the calling script doesn't depend on the underlying type, just on whether or not
117            // gas was forwarded.
118            let call_opcode_params = CallOpcodeParamsOffset {
119                gas_forwarded_offset: c.call_parameters.gas_forwarded().map(|_| 0),
120                ..CallOpcodeParamsOffset::default()
121            };
122
123            ContractCallInstructions::new(call_opcode_params)
124                .into_bytes()
125                .count()
126        })
127        .sum()
128}
129
130/// Compute how much of each asset is required based on all `CallParameters` of the `ContractCalls`
131pub(crate) fn calculate_required_asset_amounts(
132    calls: &[ContractCall],
133    base_asset_id: AssetId,
134) -> Vec<(AssetId, u64)> {
135    let call_param_assets = calls.iter().map(|call| {
136        (
137            call.call_parameters.asset_id().unwrap_or(base_asset_id),
138            call.call_parameters.amount(),
139        )
140    });
141
142    let grouped_assets = calls
143        .iter()
144        .flat_map(|call| call.custom_assets.clone())
145        .map(|((asset_id, _), amount)| (asset_id, amount))
146        .chain(call_param_assets)
147        .sorted_by_key(|(asset_id, _)| *asset_id)
148        .group_by(|(asset_id, _)| *asset_id);
149
150    grouped_assets
151        .into_iter()
152        .filter_map(|(asset_id, groups_w_same_asset_id)| {
153            let total_amount_in_group = groups_w_same_asset_id.map(|(_, amount)| amount).sum();
154
155            (total_amount_in_group != 0).then_some((asset_id, total_amount_in_group))
156        })
157        .collect()
158}
159
160/// Given a list of contract calls, create the actual opcodes used to call the contract
161pub(crate) fn get_instructions(offsets: Vec<CallOpcodeParamsOffset>) -> Vec<u8> {
162    offsets
163        .into_iter()
164        .flat_map(|offset| ContractCallInstructions::new(offset).into_bytes())
165        .chain(op::ret(RegId::ONE).to_bytes())
166        .collect()
167}
168
169pub(crate) fn build_script_data_from_contract_calls(
170    calls: &[ContractCall],
171    data_offset: usize,
172    base_asset_id: AssetId,
173) -> Result<(Vec<u8>, Vec<CallOpcodeParamsOffset>)> {
174    calls.iter().try_fold(
175        (vec![], vec![]),
176        |(mut script_data, mut param_offsets), call| {
177            let segment_offset = data_offset + script_data.len();
178            let offset = call
179                .data(base_asset_id)?
180                .encode(segment_offset, &mut script_data);
181
182            param_offsets.push(offset);
183            Ok((script_data, param_offsets))
184        },
185    )
186}
187
188/// Returns the assets and contracts that will be consumed ([`Input`]s)
189/// and created ([`Output`]s) by the transaction
190pub(crate) fn get_transaction_inputs_outputs(
191    calls: &[ContractCall],
192    asset_inputs: Vec<Input>,
193    address: &Bech32Address,
194    base_asset_id: AssetId,
195) -> (Vec<Input>, Vec<Output>) {
196    let asset_ids = extract_unique_asset_ids(&asset_inputs, base_asset_id);
197    let contract_ids = extract_unique_contract_ids(calls);
198    let num_of_contracts = contract_ids.len();
199
200    let inputs = chain!(generate_contract_inputs(contract_ids), asset_inputs).collect();
201
202    // Note the contract_outputs need to come first since the
203    // contract_inputs are referencing them via `output_index`. The node
204    // will, upon receiving our request, use `output_index` to index the
205    // `inputs` array we've sent over.
206    let outputs = chain!(
207        generate_contract_outputs(num_of_contracts),
208        generate_asset_change_outputs(address, asset_ids),
209        generate_custom_outputs(calls),
210    )
211    .collect();
212
213    (inputs, outputs)
214}
215
216fn generate_custom_outputs(calls: &[ContractCall]) -> Vec<Output> {
217    calls
218        .iter()
219        .flat_map(|call| &call.custom_assets)
220        .group_by(|custom| (custom.0 .0, custom.0 .1.clone()))
221        .into_iter()
222        .filter_map(|(asset_id_address, groups_w_same_asset_id_address)| {
223            let total_amount_in_group = groups_w_same_asset_id_address
224                .map(|(_, amount)| amount)
225                .sum::<u64>();
226            match asset_id_address.1 {
227                Some(address) => Some(Output::coin(
228                    address.into(),
229                    total_amount_in_group,
230                    asset_id_address.0,
231                )),
232                None => None,
233            }
234        })
235        .collect::<Vec<_>>()
236}
237
238fn extract_unique_asset_ids(asset_inputs: &[Input], base_asset_id: AssetId) -> HashSet<AssetId> {
239    asset_inputs
240        .iter()
241        .filter_map(|input| match input {
242            Input::ResourceSigned { resource, .. } | Input::ResourcePredicate { resource, .. } => {
243                Some(resource.coin_asset_id().unwrap_or(base_asset_id))
244            }
245            _ => None,
246        })
247        .collect()
248}
249
250fn generate_asset_change_outputs(
251    wallet_address: &Bech32Address,
252    asset_ids: HashSet<AssetId>,
253) -> Vec<Output> {
254    asset_ids
255        .into_iter()
256        .map(|asset_id| Output::change(wallet_address.into(), 0, asset_id))
257        .collect()
258}
259
260pub(crate) fn generate_contract_outputs(num_of_contracts: usize) -> Vec<Output> {
261    (0..num_of_contracts)
262        .map(|idx| Output::contract(idx as u16, Bytes32::zeroed(), Bytes32::zeroed()))
263        .collect()
264}
265
266pub(crate) fn generate_contract_inputs(contract_ids: HashSet<ContractId>) -> Vec<Input> {
267    contract_ids
268        .into_iter()
269        .enumerate()
270        .map(|(idx, contract_id)| {
271            Input::contract(
272                UtxoId::new(Bytes32::zeroed(), idx as u16),
273                Bytes32::zeroed(),
274                Bytes32::zeroed(),
275                TxPointer::default(),
276                contract_id,
277            )
278        })
279        .collect()
280}
281
282fn extract_unique_contract_ids(calls: &[ContractCall]) -> HashSet<ContractId> {
283    calls
284        .iter()
285        .flat_map(|call| {
286            call.external_contracts
287                .iter()
288                .map(|bech32| bech32.into())
289                .chain(iter::once((&call.contract_id).into()))
290        })
291        .collect()
292}
293
294pub fn is_missing_output_variables(receipts: &[Receipt]) -> bool {
295    receipts.iter().any(
296        |r| matches!(r, Receipt::Revert { ra, .. } if *ra == FAILED_TRANSFER_TO_ADDRESS_SIGNAL),
297    )
298}
299
300pub fn find_id_of_missing_contract(receipts: &[Receipt]) -> Option<Bech32ContractId> {
301    receipts.iter().find_map(|receipt| match receipt {
302        Receipt::Panic {
303            reason,
304            contract_id,
305            ..
306        } if *reason.reason() == PanicReason::ContractNotInInputs => {
307            let contract_id = contract_id
308                .expect("panic caused by a contract not in inputs must have a contract id");
309            Some(Bech32ContractId::from(contract_id))
310        }
311        _ => None,
312    })
313}
314
315#[cfg(test)]
316mod test {
317    use std::slice;
318
319    use fuels_accounts::wallet::WalletUnlocked;
320    use fuels_core::types::{
321        coin::{Coin, CoinStatus},
322        coin_type::CoinType,
323        param_types::ParamType,
324    };
325    use rand::Rng;
326
327    use super::*;
328    use crate::calls::{traits::ContractDependencyConfigurator, CallParameters};
329
330    fn new_contract_call_with_random_id() -> ContractCall {
331        ContractCall {
332            contract_id: random_bech32_contract_id(),
333            encoded_args: Ok(Default::default()),
334            encoded_selector: [0; 8].to_vec(),
335            call_parameters: Default::default(),
336            external_contracts: Default::default(),
337            output_param: ParamType::Unit,
338            is_payable: false,
339            custom_assets: Default::default(),
340        }
341    }
342
343    fn random_bech32_contract_id() -> Bech32ContractId {
344        Bech32ContractId::new("fuel", rand::thread_rng().gen::<[u8; 32]>())
345    }
346
347    #[test]
348    fn contract_input_present() {
349        let call = new_contract_call_with_random_id();
350
351        let wallet = WalletUnlocked::new_random(None);
352
353        let (inputs, _) = get_transaction_inputs_outputs(
354            slice::from_ref(&call),
355            Default::default(),
356            wallet.address(),
357            AssetId::zeroed(),
358        );
359
360        assert_eq!(
361            inputs,
362            vec![Input::contract(
363                UtxoId::new(Bytes32::zeroed(), 0),
364                Bytes32::zeroed(),
365                Bytes32::zeroed(),
366                TxPointer::default(),
367                call.contract_id.into(),
368            )]
369        );
370    }
371
372    #[test]
373    fn contract_input_is_not_duplicated() {
374        let call = new_contract_call_with_random_id();
375        let call_w_same_contract =
376            new_contract_call_with_random_id().with_contract_id(call.contract_id.clone());
377
378        let wallet = WalletUnlocked::new_random(None);
379
380        let calls = [call, call_w_same_contract];
381
382        let (inputs, _) = get_transaction_inputs_outputs(
383            &calls,
384            Default::default(),
385            wallet.address(),
386            AssetId::zeroed(),
387        );
388
389        assert_eq!(
390            inputs,
391            vec![Input::contract(
392                UtxoId::new(Bytes32::zeroed(), 0),
393                Bytes32::zeroed(),
394                Bytes32::zeroed(),
395                TxPointer::default(),
396                calls[0].contract_id.clone().into(),
397            )]
398        );
399    }
400
401    #[test]
402    fn contract_output_present() {
403        let call = new_contract_call_with_random_id();
404
405        let wallet = WalletUnlocked::new_random(None);
406
407        let (_, outputs) = get_transaction_inputs_outputs(
408            &[call],
409            Default::default(),
410            wallet.address(),
411            AssetId::zeroed(),
412        );
413
414        assert_eq!(
415            outputs,
416            vec![Output::contract(0, Bytes32::zeroed(), Bytes32::zeroed())]
417        );
418    }
419
420    #[test]
421    fn external_contract_input_present() {
422        // given
423        let external_contract_id = random_bech32_contract_id();
424        let call = new_contract_call_with_random_id()
425            .with_external_contracts(vec![external_contract_id.clone()]);
426
427        let wallet = WalletUnlocked::new_random(None);
428
429        // when
430        let (inputs, _) = get_transaction_inputs_outputs(
431            slice::from_ref(&call),
432            Default::default(),
433            wallet.address(),
434            AssetId::zeroed(),
435        );
436
437        // then
438        let mut expected_contract_ids: HashSet<ContractId> =
439            [call.contract_id.into(), external_contract_id.into()].into();
440
441        for (index, input) in inputs.into_iter().enumerate() {
442            match input {
443                Input::Contract {
444                    utxo_id,
445                    balance_root,
446                    state_root,
447                    tx_pointer,
448                    contract_id,
449                } => {
450                    assert_eq!(utxo_id, UtxoId::new(Bytes32::zeroed(), index as u16));
451                    assert_eq!(balance_root, Bytes32::zeroed());
452                    assert_eq!(state_root, Bytes32::zeroed());
453                    assert_eq!(tx_pointer, TxPointer::default());
454                    assert!(expected_contract_ids.contains(&contract_id));
455                    expected_contract_ids.remove(&contract_id);
456                }
457                _ => {
458                    panic!("expected only inputs of type `Input::Contract`");
459                }
460            }
461        }
462    }
463
464    #[test]
465    fn external_contract_output_present() {
466        // given
467        let external_contract_id = random_bech32_contract_id();
468        let call =
469            new_contract_call_with_random_id().with_external_contracts(vec![external_contract_id]);
470
471        let wallet = WalletUnlocked::new_random(None);
472
473        // when
474        let (_, outputs) = get_transaction_inputs_outputs(
475            &[call],
476            Default::default(),
477            wallet.address(),
478            AssetId::zeroed(),
479        );
480
481        // then
482        let expected_outputs = (0..=1)
483            .map(|i| Output::contract(i, Bytes32::zeroed(), Bytes32::zeroed()))
484            .collect::<Vec<_>>();
485
486        assert_eq!(outputs, expected_outputs);
487    }
488
489    #[test]
490    fn change_per_asset_id_added() {
491        // given
492        let asset_ids = [AssetId::zeroed(), AssetId::from([1; 32])];
493
494        let coins = asset_ids
495            .into_iter()
496            .map(|asset_id| {
497                let coin = CoinType::Coin(Coin {
498                    amount: 100,
499                    block_created: 0u32,
500                    asset_id,
501                    utxo_id: Default::default(),
502                    owner: Default::default(),
503                    status: CoinStatus::Unspent,
504                });
505                Input::resource_signed(coin)
506            })
507            .collect();
508        let call = new_contract_call_with_random_id();
509
510        let wallet = WalletUnlocked::new_random(None);
511
512        // when
513        let (_, outputs) =
514            get_transaction_inputs_outputs(&[call], coins, wallet.address(), AssetId::zeroed());
515
516        // then
517        let change_outputs: HashSet<Output> = outputs[1..].iter().cloned().collect();
518
519        let expected_change_outputs = asset_ids
520            .into_iter()
521            .map(|asset_id| Output::Change {
522                to: wallet.address().into(),
523                amount: 0,
524                asset_id,
525            })
526            .collect();
527
528        assert_eq!(change_outputs, expected_change_outputs);
529    }
530
531    #[test]
532    fn will_collate_same_asset_ids() {
533        let asset_id_1 = AssetId::from([1; 32]);
534        let asset_id_2 = AssetId::from([2; 32]);
535
536        let calls = [
537            (asset_id_1, 100),
538            (asset_id_2, 200),
539            (asset_id_1, 300),
540            (asset_id_2, 400),
541        ]
542        .map(|(asset_id, amount)| {
543            CallParameters::default()
544                .with_amount(amount)
545                .with_asset_id(asset_id)
546        })
547        .map(|call_parameters| {
548            new_contract_call_with_random_id().with_call_parameters(call_parameters)
549        });
550
551        let asset_id_amounts = calculate_required_asset_amounts(&calls, AssetId::zeroed());
552
553        let expected_asset_id_amounts = [(asset_id_1, 400), (asset_id_2, 600)].into();
554
555        assert_eq!(
556            asset_id_amounts.into_iter().collect::<HashSet<_>>(),
557            expected_asset_id_amounts
558        )
559    }
560
561    mod compute_calls_instructions_len {
562        use fuel_asm::Instruction;
563        use fuels_core::types::param_types::{EnumVariants, ParamType};
564
565        use super::new_contract_call_with_random_id;
566        use crate::calls::utils::compute_calls_instructions_len;
567
568        // movi, movi, lw, movi + call (for gas)
569        const BASE_INSTRUCTION_COUNT: usize = 5;
570        // 2 instructions (movi and lw) added in get_single_call_instructions when gas_offset is set
571        const GAS_OFFSET_INSTRUCTION_COUNT: usize = 2;
572
573        #[test]
574        fn test_simple() {
575            let call = new_contract_call_with_random_id();
576            let instructions_len = compute_calls_instructions_len(&[call]);
577            assert_eq!(instructions_len, Instruction::SIZE * BASE_INSTRUCTION_COUNT);
578        }
579
580        #[test]
581        fn test_with_gas_offset() {
582            let mut call = new_contract_call_with_random_id();
583            call.call_parameters = call.call_parameters.with_gas_forwarded(0);
584            let instructions_len = compute_calls_instructions_len(&[call]);
585            assert_eq!(
586                instructions_len,
587                Instruction::SIZE * (BASE_INSTRUCTION_COUNT + GAS_OFFSET_INSTRUCTION_COUNT)
588            );
589        }
590
591        #[test]
592        fn test_with_enum_with_only_non_heap_variants() {
593            let mut call = new_contract_call_with_random_id();
594            call.output_param = ParamType::Enum {
595                name: "".to_string(),
596                enum_variants: EnumVariants::new(vec![
597                    ("".to_string(), ParamType::Bool),
598                    ("".to_string(), ParamType::U8),
599                ])
600                .unwrap(),
601                generics: Vec::new(),
602            };
603            let instructions_len = compute_calls_instructions_len(&[call]);
604            assert_eq!(
605                instructions_len,
606                // no extra instructions if there are no heap type variants
607                Instruction::SIZE * BASE_INSTRUCTION_COUNT
608            );
609        }
610    }
611}