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
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: &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 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.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
271pub(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
287pub(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 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 let (inputs, _) = get_transaction_inputs_outputs(
461 slice::from_ref(&call),
462 Default::default(),
463 signer.address(),
464 AssetId::zeroed(),
465 );
466
467 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 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 let (_, outputs) = get_transaction_inputs_outputs(
505 &[call],
506 Default::default(),
507 signer.address(),
508 AssetId::zeroed(),
509 );
510
511 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 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 let (_, outputs) =
544 get_transaction_inputs_outputs(&[call], coins, signer.address(), AssetId::zeroed());
545
546 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 const BASE_INSTRUCTION_COUNT: usize = 5;
600 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 Instruction::SIZE * BASE_INSTRUCTION_COUNT
638 );
639 }
640 }
641}