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