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::{RegId, op};
5use fuel_tx::{Output, PanicReason, Receipt, TxPointer, UtxoId};
6use fuels_accounts::Account;
7use fuels_core::{
8    offsets::call_script_data_offset,
9    types::{
10        Address, AssetId, Bytes32, ContractId,
11        errors::{Context, Result},
12        input::Input,
13        transaction::{ScriptTransaction, TxPolicies},
14        transaction_builders::{
15            BuildableTransaction, ScriptTransactionBuilder, TransactionBuilder,
16            VariableOutputPolicy,
17        },
18    },
19};
20use itertools::{Itertools, chain};
21
22use crate::{
23    DEFAULT_MAX_FEE_ESTIMATION_TOLERANCE,
24    assembly::contract_call::{CallOpcodeParamsOffset, ContractCallInstructions},
25    calls::ContractCall,
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
100        .adjust_for_fee(&mut tb, used_base_amount)
101        .await
102        .context("failed to adjust inputs to cover for missing base asset")?;
103
104    tb.build(account.try_provider()?).await
105}
106
107/// Compute the length of the calling scripts for the two types of contract calls: those that return
108/// a heap type, and those that don't.
109fn compute_calls_instructions_len(calls: &[ContractCall]) -> usize {
110    calls
111        .iter()
112        .map(|c| {
113            // Use placeholder for `call_param_offsets` and `output_param_type`, because the length of
114            // the calling script doesn't depend on the underlying type, just on whether or not
115            // gas was forwarded.
116            let call_opcode_params = CallOpcodeParamsOffset {
117                gas_forwarded_offset: c.call_parameters.gas_forwarded().map(|_| 0),
118                ..CallOpcodeParamsOffset::default()
119            };
120
121            ContractCallInstructions::new(call_opcode_params)
122                .into_bytes()
123                .count()
124        })
125        .sum()
126}
127
128/// Compute how much of each asset is required based on all `CallParameters` of the `ContractCalls`
129pub(crate) fn calculate_required_asset_amounts(
130    calls: &[ContractCall],
131    base_asset_id: AssetId,
132) -> Vec<(AssetId, u128)> {
133    let call_param_assets = calls.iter().map(|call| {
134        (
135            call.call_parameters.asset_id().unwrap_or(base_asset_id),
136            call.call_parameters.amount(),
137        )
138    });
139
140    let grouped_assets = calls
141        .iter()
142        .flat_map(|call| call.custom_assets.clone())
143        .map(|((asset_id, _), amount)| (asset_id, amount))
144        .chain(call_param_assets)
145        .sorted_by_key(|(asset_id, _)| *asset_id)
146        .group_by(|(asset_id, _)| *asset_id);
147
148    grouped_assets
149        .into_iter()
150        .filter_map(|(asset_id, groups_w_same_asset_id)| {
151            let total_amount_in_group = groups_w_same_asset_id
152                .map(|(_, amount)| u128::from(amount))
153                .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: Address,
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    // Custom `Inputs` and `Outputs` should be placed before other inputs and outputs.
201    let custom_inputs = calls.iter().flat_map(|c| c.inputs.clone()).collect_vec();
202    let custom_inputs_len = custom_inputs.len();
203    let custom_outputs = calls.iter().flat_map(|c| c.outputs.clone()).collect_vec();
204
205    let inputs = chain!(
206        custom_inputs,
207        generate_contract_inputs(contract_ids, custom_outputs.len()),
208        asset_inputs
209    )
210    .collect();
211
212    // Note the contract_outputs are placed after the custom outputs and
213    // the contract_inputs are referencing them via `output_index`. The
214    // node will, upon receiving our request, use `output_index` to index
215    // the `inputs` array we've sent over.
216    let outputs = chain!(
217        custom_outputs,
218        generate_contract_outputs(num_of_contracts, custom_inputs_len),
219        generate_asset_change_outputs(address, asset_ids),
220        generate_custom_outputs(calls),
221    )
222    .collect();
223
224    (inputs, outputs)
225}
226
227fn generate_custom_outputs(calls: &[ContractCall]) -> Vec<Output> {
228    calls
229        .iter()
230        .flat_map(|call| &call.custom_assets)
231        .group_by(|custom| (custom.0.0, custom.0.1))
232        .into_iter()
233        .filter_map(|(asset_id_address, groups_w_same_asset_id_address)| {
234            let total_amount_in_group = groups_w_same_asset_id_address
235                .map(|(_, amount)| amount)
236                .sum::<u64>();
237
238            asset_id_address
239                .1
240                .map(|address| Output::coin(address, total_amount_in_group, asset_id_address.0))
241        })
242        .collect::<Vec<_>>()
243}
244
245fn extract_unique_asset_ids(asset_inputs: &[Input], base_asset_id: AssetId) -> HashSet<AssetId> {
246    asset_inputs
247        .iter()
248        .filter_map(|input| match input {
249            Input::ResourceSigned { resource, .. } | Input::ResourcePredicate { resource, .. } => {
250                Some(resource.coin_asset_id().unwrap_or(base_asset_id))
251            }
252            _ => None,
253        })
254        .collect()
255}
256
257fn generate_asset_change_outputs(
258    wallet_address: Address,
259    asset_ids: HashSet<AssetId>,
260) -> Vec<Output> {
261    asset_ids
262        .into_iter()
263        .map(|asset_id| Output::change(wallet_address, 0, asset_id))
264        .collect()
265}
266
267/// Generate contract outputs taking in consideration already existing inputs
268pub(crate) fn generate_contract_outputs(
269    num_of_contracts: usize,
270    num_current_inputs: usize,
271) -> Vec<Output> {
272    (0..num_of_contracts)
273        .map(|idx| {
274            Output::contract(
275                (idx + num_current_inputs) as u16,
276                Bytes32::zeroed(),
277                Bytes32::zeroed(),
278            )
279        })
280        .collect()
281}
282
283/// Generate contract inputs taking in consideration already existing outputs
284pub(crate) fn generate_contract_inputs(
285    contract_ids: HashSet<ContractId>,
286    num_current_outputs: usize,
287) -> Vec<Input> {
288    contract_ids
289        .into_iter()
290        .enumerate()
291        .map(|(idx, contract_id)| {
292            Input::contract(
293                UtxoId::new(Bytes32::zeroed(), (idx + num_current_outputs) as u16),
294                Bytes32::zeroed(),
295                Bytes32::zeroed(),
296                TxPointer::default(),
297                contract_id,
298            )
299        })
300        .collect()
301}
302
303fn extract_unique_contract_ids(calls: &[ContractCall]) -> HashSet<ContractId> {
304    calls
305        .iter()
306        .flat_map(|call| {
307            call.external_contracts
308                .iter()
309                .copied()
310                .chain(iter::once(call.contract_id))
311        })
312        .collect()
313}
314
315pub fn is_missing_output_variables(receipts: &[Receipt]) -> bool {
316    receipts.iter().any(
317        |r| matches!(r, Receipt::Revert { ra, .. } if *ra == FAILED_TRANSFER_TO_ADDRESS_SIGNAL),
318    )
319}
320
321pub fn find_ids_of_missing_contracts(receipts: &[Receipt]) -> Vec<ContractId> {
322    receipts
323        .iter()
324        .filter_map(|receipt| match receipt {
325            Receipt::Panic {
326                reason,
327                contract_id,
328                ..
329            } if *reason.reason() == PanicReason::ContractNotInInputs => {
330                let contract_id = contract_id
331                    .expect("panic caused by a contract not in inputs must have a contract id");
332                Some(contract_id)
333            }
334            _ => None,
335        })
336        .collect()
337}
338
339#[cfg(test)]
340mod test {
341    use std::slice;
342
343    use fuels_accounts::signers::private_key::PrivateKeySigner;
344    use fuels_core::types::{coin::Coin, coin_type::CoinType, param_types::ParamType};
345    use rand::{Rng, thread_rng};
346
347    use super::*;
348    use crate::calls::{CallParameters, traits::ContractDependencyConfigurator};
349
350    fn new_contract_call_with_random_id() -> ContractCall {
351        ContractCall {
352            contract_id: random_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_contract_id() -> ContractId {
366        rand::thread_rng().r#gen()
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,
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);
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,
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_contract_id();
446        let call =
447            new_contract_call_with_random_id().with_external_contracts(vec![external_contract_id]);
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, external_contract_id].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_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                    asset_id,
522                    utxo_id: Default::default(),
523                    owner: Default::default(),
524                });
525                Input::resource_signed(coin)
526            })
527            .collect();
528        let call = new_contract_call_with_random_id();
529
530        let signer = PrivateKeySigner::random(&mut thread_rng());
531
532        // when
533        let (_, outputs) =
534            get_transaction_inputs_outputs(&[call], coins, signer.address(), AssetId::zeroed());
535
536        // then
537        let change_outputs: HashSet<Output> = outputs[1..].iter().cloned().collect();
538
539        let expected_change_outputs = asset_ids
540            .into_iter()
541            .map(|asset_id| Output::Change {
542                to: signer.address(),
543                amount: 0,
544                asset_id,
545            })
546            .collect();
547
548        assert_eq!(change_outputs, expected_change_outputs);
549    }
550
551    #[test]
552    fn will_collate_same_asset_ids() {
553        let asset_id_1 = AssetId::from([1; 32]);
554        let asset_id_2 = AssetId::from([2; 32]);
555
556        let calls = [
557            (asset_id_1, 100),
558            (asset_id_2, 200),
559            (asset_id_1, 300),
560            (asset_id_2, 400),
561        ]
562        .map(|(asset_id, amount)| {
563            CallParameters::default()
564                .with_amount(amount)
565                .with_asset_id(asset_id)
566        })
567        .map(|call_parameters| {
568            new_contract_call_with_random_id().with_call_parameters(call_parameters)
569        });
570
571        let asset_id_amounts = calculate_required_asset_amounts(&calls, AssetId::zeroed());
572
573        let expected_asset_id_amounts = [(asset_id_1, 400), (asset_id_2, 600)].into();
574
575        assert_eq!(
576            asset_id_amounts.into_iter().collect::<HashSet<_>>(),
577            expected_asset_id_amounts
578        )
579    }
580
581    mod compute_calls_instructions_len {
582        use fuel_asm::Instruction;
583        use fuels_core::types::param_types::{EnumVariants, ParamType};
584
585        use super::new_contract_call_with_random_id;
586        use crate::calls::utils::compute_calls_instructions_len;
587
588        // movi, movi, lw, movi + call (for gas)
589        const BASE_INSTRUCTION_COUNT: usize = 5;
590        // 2 instructions (movi and lw) added in get_single_call_instructions when gas_offset is set
591        const GAS_OFFSET_INSTRUCTION_COUNT: usize = 2;
592
593        #[test]
594        fn test_simple() {
595            let call = new_contract_call_with_random_id();
596            let instructions_len = compute_calls_instructions_len(&[call]);
597            assert_eq!(instructions_len, Instruction::SIZE * BASE_INSTRUCTION_COUNT);
598        }
599
600        #[test]
601        fn test_with_gas_offset() {
602            let mut call = new_contract_call_with_random_id();
603            call.call_parameters = call.call_parameters.with_gas_forwarded(0);
604            let instructions_len = compute_calls_instructions_len(&[call]);
605            assert_eq!(
606                instructions_len,
607                Instruction::SIZE * (BASE_INSTRUCTION_COUNT + GAS_OFFSET_INSTRUCTION_COUNT)
608            );
609        }
610
611        #[test]
612        fn test_with_enum_with_only_non_heap_variants() {
613            let mut call = new_contract_call_with_random_id();
614            call.output_param = ParamType::Enum {
615                name: "".to_string(),
616                enum_variants: EnumVariants::new(vec![
617                    ("".to_string(), ParamType::Bool),
618                    ("".to_string(), ParamType::U8),
619                ])
620                .unwrap(),
621                generics: Vec::new(),
622            };
623            let instructions_len = compute_calls_instructions_len(&[call]);
624            assert_eq!(
625                instructions_len,
626                // no extra instructions if there are no heap type variants
627                Instruction::SIZE * BASE_INSTRUCTION_COUNT
628            );
629        }
630    }
631}