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
32pub(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
69pub(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
95fn compute_calls_instructions_len(calls: &[ContractCall]) -> usize {
98 calls
99 .iter()
100 .map(|c| {
101 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
116pub 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
148pub(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
176pub(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 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 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
255pub(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
271pub(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 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 let (inputs, _) = get_transaction_inputs_outputs(
441 slice::from_ref(&call),
442 Default::default(),
443 signer.address(),
444 AssetId::zeroed(),
445 );
446
447 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 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 let (_, outputs) = get_transaction_inputs_outputs(
485 &[call],
486 Default::default(),
487 signer.address(),
488 AssetId::zeroed(),
489 );
490
491 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 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 let (_, outputs) =
522 get_transaction_inputs_outputs(&[call], coins, signer.address(), AssetId::zeroed());
523
524 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 const BASE_INSTRUCTION_COUNT: usize = 5;
578 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 Instruction::SIZE * BASE_INSTRUCTION_COUNT
616 );
617 }
618 }
619}