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::{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::{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: &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    // 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.clone()))
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            match asset_id_address.1 {
238                Some(address) => Some(Output::coin(
239                    address.into(),
240                    total_amount_in_group,
241                    asset_id_address.0,
242                )),
243                None => None,
244            }
245        })
246        .collect::<Vec<_>>()
247}
248
249fn extract_unique_asset_ids(asset_inputs: &[Input], base_asset_id: AssetId) -> HashSet<AssetId> {
250    asset_inputs
251        .iter()
252        .filter_map(|input| match input {
253            Input::ResourceSigned { resource, .. } | Input::ResourcePredicate { resource, .. } => {
254                Some(resource.coin_asset_id().unwrap_or(base_asset_id))
255            }
256            _ => None,
257        })
258        .collect()
259}
260
261fn generate_asset_change_outputs(
262    wallet_address: &Bech32Address,
263    asset_ids: HashSet<AssetId>,
264) -> Vec<Output> {
265    asset_ids
266        .into_iter()
267        .map(|asset_id| Output::change(wallet_address.into(), 0, asset_id))
268        .collect()
269}
270
271/// Generate contract outputs taking in consideration already existing inputs
272pub(crate) fn generate_contract_outputs(
273    num_of_contracts: usize,
274    num_current_inputs: usize,
275) -> Vec<Output> {
276    (0..num_of_contracts)
277        .map(|idx| {
278            Output::contract(
279                (idx + num_current_inputs) as u16,
280                Bytes32::zeroed(),
281                Bytes32::zeroed(),
282            )
283        })
284        .collect()
285}
286
287/// Generate contract inputs taking in consideration already existing outputs
288pub(crate) fn generate_contract_inputs(
289    contract_ids: HashSet<ContractId>,
290    num_current_outputs: usize,
291) -> Vec<Input> {
292    contract_ids
293        .into_iter()
294        .enumerate()
295        .map(|(idx, contract_id)| {
296            Input::contract(
297                UtxoId::new(Bytes32::zeroed(), (idx + num_current_outputs) as u16),
298                Bytes32::zeroed(),
299                Bytes32::zeroed(),
300                TxPointer::default(),
301                contract_id,
302            )
303        })
304        .collect()
305}
306
307fn extract_unique_contract_ids(calls: &[ContractCall]) -> HashSet<ContractId> {
308    calls
309        .iter()
310        .flat_map(|call| {
311            call.external_contracts
312                .iter()
313                .map(|bech32| bech32.into())
314                .chain(iter::once((&call.contract_id).into()))
315        })
316        .collect()
317}
318
319pub fn is_missing_output_variables(receipts: &[Receipt]) -> bool {
320    receipts.iter().any(
321        |r| matches!(r, Receipt::Revert { ra, .. } if *ra == FAILED_TRANSFER_TO_ADDRESS_SIGNAL),
322    )
323}
324
325pub fn find_ids_of_missing_contracts(receipts: &[Receipt]) -> Vec<Bech32ContractId> {
326    receipts
327        .iter()
328        .filter_map(|receipt| match receipt {
329            Receipt::Panic {
330                reason,
331                contract_id,
332                ..
333            } if *reason.reason() == PanicReason::ContractNotInInputs => {
334                let contract_id = contract_id
335                    .expect("panic caused by a contract not in inputs must have a contract id");
336                Some(Bech32ContractId::from(contract_id))
337            }
338            _ => None,
339        })
340        .collect()
341}
342
343#[cfg(test)]
344mod test {
345    use std::slice;
346
347    use fuels_accounts::signers::private_key::PrivateKeySigner;
348    use fuels_core::types::{
349        coin::{Coin, CoinStatus},
350        coin_type::CoinType,
351        param_types::ParamType,
352    };
353    use rand::{Rng, thread_rng};
354
355    use super::*;
356    use crate::calls::{CallParameters, traits::ContractDependencyConfigurator};
357
358    fn new_contract_call_with_random_id() -> ContractCall {
359        ContractCall {
360            contract_id: random_bech32_contract_id(),
361            encoded_args: Ok(Default::default()),
362            encoded_selector: [0; 8].to_vec(),
363            call_parameters: Default::default(),
364            external_contracts: Default::default(),
365            output_param: ParamType::Unit,
366            is_payable: false,
367            custom_assets: Default::default(),
368            inputs: vec![],
369            outputs: vec![],
370        }
371    }
372
373    fn random_bech32_contract_id() -> Bech32ContractId {
374        Bech32ContractId::new("fuel", rand::thread_rng().r#gen::<[u8; 32]>())
375    }
376
377    #[test]
378    fn contract_input_present() {
379        let call = new_contract_call_with_random_id();
380
381        let signer = PrivateKeySigner::random(&mut thread_rng());
382
383        let (inputs, _) = get_transaction_inputs_outputs(
384            slice::from_ref(&call),
385            Default::default(),
386            signer.address(),
387            AssetId::zeroed(),
388        );
389
390        assert_eq!(
391            inputs,
392            vec![Input::contract(
393                UtxoId::new(Bytes32::zeroed(), 0),
394                Bytes32::zeroed(),
395                Bytes32::zeroed(),
396                TxPointer::default(),
397                call.contract_id.into(),
398            )]
399        );
400    }
401
402    #[test]
403    fn contract_input_is_not_duplicated() {
404        let call = new_contract_call_with_random_id();
405        let call_w_same_contract =
406            new_contract_call_with_random_id().with_contract_id(call.contract_id.clone());
407
408        let signer = PrivateKeySigner::random(&mut thread_rng());
409
410        let calls = [call, call_w_same_contract];
411
412        let (inputs, _) = get_transaction_inputs_outputs(
413            &calls,
414            Default::default(),
415            signer.address(),
416            AssetId::zeroed(),
417        );
418
419        assert_eq!(
420            inputs,
421            vec![Input::contract(
422                UtxoId::new(Bytes32::zeroed(), 0),
423                Bytes32::zeroed(),
424                Bytes32::zeroed(),
425                TxPointer::default(),
426                calls[0].contract_id.clone().into(),
427            )]
428        );
429    }
430
431    #[test]
432    fn contract_output_present() {
433        let call = new_contract_call_with_random_id();
434
435        let signer = PrivateKeySigner::random(&mut thread_rng());
436
437        let (_, outputs) = get_transaction_inputs_outputs(
438            &[call],
439            Default::default(),
440            signer.address(),
441            AssetId::zeroed(),
442        );
443
444        assert_eq!(
445            outputs,
446            vec![Output::contract(0, Bytes32::zeroed(), Bytes32::zeroed())]
447        );
448    }
449
450    #[test]
451    fn external_contract_input_present() {
452        // given
453        let external_contract_id = random_bech32_contract_id();
454        let call = new_contract_call_with_random_id()
455            .with_external_contracts(vec![external_contract_id.clone()]);
456
457        let signer = PrivateKeySigner::random(&mut thread_rng());
458
459        // when
460        let (inputs, _) = get_transaction_inputs_outputs(
461            slice::from_ref(&call),
462            Default::default(),
463            signer.address(),
464            AssetId::zeroed(),
465        );
466
467        // then
468        let mut expected_contract_ids: HashSet<ContractId> =
469            [call.contract_id.into(), external_contract_id.into()].into();
470
471        for (index, input) in inputs.into_iter().enumerate() {
472            match input {
473                Input::Contract {
474                    utxo_id,
475                    balance_root,
476                    state_root,
477                    tx_pointer,
478                    contract_id,
479                } => {
480                    assert_eq!(utxo_id, UtxoId::new(Bytes32::zeroed(), index as u16));
481                    assert_eq!(balance_root, Bytes32::zeroed());
482                    assert_eq!(state_root, Bytes32::zeroed());
483                    assert_eq!(tx_pointer, TxPointer::default());
484                    assert!(expected_contract_ids.contains(&contract_id));
485                    expected_contract_ids.remove(&contract_id);
486                }
487                _ => {
488                    panic!("expected only inputs of type `Input::Contract`");
489                }
490            }
491        }
492    }
493
494    #[test]
495    fn external_contract_output_present() {
496        // given
497        let external_contract_id = random_bech32_contract_id();
498        let call =
499            new_contract_call_with_random_id().with_external_contracts(vec![external_contract_id]);
500
501        let signer = PrivateKeySigner::random(&mut thread_rng());
502
503        // when
504        let (_, outputs) = get_transaction_inputs_outputs(
505            &[call],
506            Default::default(),
507            signer.address(),
508            AssetId::zeroed(),
509        );
510
511        // then
512        let expected_outputs = (0..=1)
513            .map(|i| Output::contract(i, Bytes32::zeroed(), Bytes32::zeroed()))
514            .collect::<Vec<_>>();
515
516        assert_eq!(outputs, expected_outputs);
517    }
518
519    #[test]
520    fn change_per_asset_id_added() {
521        // given
522        let asset_ids = [AssetId::zeroed(), AssetId::from([1; 32])];
523
524        let coins = asset_ids
525            .into_iter()
526            .map(|asset_id| {
527                let coin = CoinType::Coin(Coin {
528                    amount: 100,
529                    block_created: 0u32,
530                    asset_id,
531                    utxo_id: Default::default(),
532                    owner: Default::default(),
533                    status: CoinStatus::Unspent,
534                });
535                Input::resource_signed(coin)
536            })
537            .collect();
538        let call = new_contract_call_with_random_id();
539
540        let signer = PrivateKeySigner::random(&mut thread_rng());
541
542        // when
543        let (_, outputs) =
544            get_transaction_inputs_outputs(&[call], coins, signer.address(), AssetId::zeroed());
545
546        // then
547        let change_outputs: HashSet<Output> = outputs[1..].iter().cloned().collect();
548
549        let expected_change_outputs = asset_ids
550            .into_iter()
551            .map(|asset_id| Output::Change {
552                to: signer.address().into(),
553                amount: 0,
554                asset_id,
555            })
556            .collect();
557
558        assert_eq!(change_outputs, expected_change_outputs);
559    }
560
561    #[test]
562    fn will_collate_same_asset_ids() {
563        let asset_id_1 = AssetId::from([1; 32]);
564        let asset_id_2 = AssetId::from([2; 32]);
565
566        let calls = [
567            (asset_id_1, 100),
568            (asset_id_2, 200),
569            (asset_id_1, 300),
570            (asset_id_2, 400),
571        ]
572        .map(|(asset_id, amount)| {
573            CallParameters::default()
574                .with_amount(amount)
575                .with_asset_id(asset_id)
576        })
577        .map(|call_parameters| {
578            new_contract_call_with_random_id().with_call_parameters(call_parameters)
579        });
580
581        let asset_id_amounts = calculate_required_asset_amounts(&calls, AssetId::zeroed());
582
583        let expected_asset_id_amounts = [(asset_id_1, 400), (asset_id_2, 600)].into();
584
585        assert_eq!(
586            asset_id_amounts.into_iter().collect::<HashSet<_>>(),
587            expected_asset_id_amounts
588        )
589    }
590
591    mod compute_calls_instructions_len {
592        use fuel_asm::Instruction;
593        use fuels_core::types::param_types::{EnumVariants, ParamType};
594
595        use super::new_contract_call_with_random_id;
596        use crate::calls::utils::compute_calls_instructions_len;
597
598        // movi, movi, lw, movi + call (for gas)
599        const BASE_INSTRUCTION_COUNT: usize = 5;
600        // 2 instructions (movi and lw) added in get_single_call_instructions when gas_offset is set
601        const GAS_OFFSET_INSTRUCTION_COUNT: usize = 2;
602
603        #[test]
604        fn test_simple() {
605            let call = new_contract_call_with_random_id();
606            let instructions_len = compute_calls_instructions_len(&[call]);
607            assert_eq!(instructions_len, Instruction::SIZE * BASE_INSTRUCTION_COUNT);
608        }
609
610        #[test]
611        fn test_with_gas_offset() {
612            let mut call = new_contract_call_with_random_id();
613            call.call_parameters = call.call_parameters.with_gas_forwarded(0);
614            let instructions_len = compute_calls_instructions_len(&[call]);
615            assert_eq!(
616                instructions_len,
617                Instruction::SIZE * (BASE_INSTRUCTION_COUNT + GAS_OFFSET_INSTRUCTION_COUNT)
618            );
619        }
620
621        #[test]
622        fn test_with_enum_with_only_non_heap_variants() {
623            let mut call = new_contract_call_with_random_id();
624            call.output_param = ParamType::Enum {
625                name: "".to_string(),
626                enum_variants: EnumVariants::new(vec![
627                    ("".to_string(), ParamType::Bool),
628                    ("".to_string(), ParamType::U8),
629                ])
630                .unwrap(),
631                generics: Vec::new(),
632            };
633            let instructions_len = compute_calls_instructions_len(&[call]);
634            assert_eq!(
635                instructions_len,
636                // no extra instructions if there are no heap type variants
637                Instruction::SIZE * BASE_INSTRUCTION_COUNT
638            );
639        }
640    }
641}