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