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
32pub(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 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
81pub(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
107fn compute_calls_instructions_len(calls: &[ContractCall]) -> usize {
110 calls
111 .iter()
112 .map(|c| {
113 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
128pub(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
160pub(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
188pub(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 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 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
267pub(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
283pub(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 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 let (inputs, _) = get_transaction_inputs_outputs(
453 slice::from_ref(&call),
454 Default::default(),
455 signer.address(),
456 AssetId::zeroed(),
457 );
458
459 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 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 let (_, outputs) = get_transaction_inputs_outputs(
497 &[call],
498 Default::default(),
499 signer.address(),
500 AssetId::zeroed(),
501 );
502
503 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 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 let (_, outputs) =
534 get_transaction_inputs_outputs(&[call], coins, signer.address(), AssetId::zeroed());
535
536 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 const BASE_INSTRUCTION_COUNT: usize = 5;
590 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 Instruction::SIZE * BASE_INSTRUCTION_COUNT
628 );
629 }
630 }
631}