1use std::{collections::HashSet, iter, vec};
2
3use fuel_abi_types::error_codes::FAILED_TRANSFER_TO_ADDRESS_SIGNAL;
4use fuel_asm::{op, RegId};
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::Result,
12 input::Input,
13 transaction::{ScriptTransaction, TxPolicies},
14 transaction_builders::{
15 BuildableTransaction, ScriptTransactionBuilder, TransactionBuilder,
16 VariableOutputPolicy,
17 },
18 },
19};
20use itertools::{chain, Itertools};
21
22use crate::{
23 assembly::contract_call::{CallOpcodeParamsOffset, ContractCallInstructions},
24 calls::ContractCall,
25 DEFAULT_MAX_FEE_ESTIMATION_TOLERANCE,
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.adjust_for_fee(&mut tb, used_base_amount).await?;
100
101 tb.build(account.try_provider()?).await
102}
103
104fn compute_calls_instructions_len(calls: &[ContractCall]) -> usize {
107 calls
108 .iter()
109 .map(|c| {
110 let call_opcode_params = CallOpcodeParamsOffset {
114 gas_forwarded_offset: c.call_parameters.gas_forwarded().map(|_| 0),
115 ..CallOpcodeParamsOffset::default()
116 };
117
118 ContractCallInstructions::new(call_opcode_params)
119 .into_bytes()
120 .count()
121 })
122 .sum()
123}
124
125pub(crate) fn calculate_required_asset_amounts(
127 calls: &[ContractCall],
128 base_asset_id: AssetId,
129) -> Vec<(AssetId, u64)> {
130 let call_param_assets = calls.iter().map(|call| {
131 (
132 call.call_parameters.asset_id().unwrap_or(base_asset_id),
133 call.call_parameters.amount(),
134 )
135 });
136
137 let grouped_assets = calls
138 .iter()
139 .flat_map(|call| call.custom_assets.clone())
140 .map(|((asset_id, _), amount)| (asset_id, amount))
141 .chain(call_param_assets)
142 .sorted_by_key(|(asset_id, _)| *asset_id)
143 .group_by(|(asset_id, _)| *asset_id);
144
145 grouped_assets
146 .into_iter()
147 .filter_map(|(asset_id, groups_w_same_asset_id)| {
148 let total_amount_in_group = groups_w_same_asset_id.map(|(_, amount)| amount).sum();
149
150 (total_amount_in_group != 0).then_some((asset_id, total_amount_in_group))
151 })
152 .collect()
153}
154
155pub(crate) fn get_instructions(offsets: Vec<CallOpcodeParamsOffset>) -> Vec<u8> {
157 offsets
158 .into_iter()
159 .flat_map(|offset| ContractCallInstructions::new(offset).into_bytes())
160 .chain(op::ret(RegId::ONE).to_bytes())
161 .collect()
162}
163
164pub(crate) fn build_script_data_from_contract_calls(
165 calls: &[ContractCall],
166 data_offset: usize,
167 base_asset_id: AssetId,
168) -> Result<(Vec<u8>, Vec<CallOpcodeParamsOffset>)> {
169 calls.iter().try_fold(
170 (vec![], vec![]),
171 |(mut script_data, mut param_offsets), call| {
172 let segment_offset = data_offset + script_data.len();
173 let offset = call
174 .data(base_asset_id)?
175 .encode(segment_offset, &mut script_data);
176
177 param_offsets.push(offset);
178 Ok((script_data, param_offsets))
179 },
180 )
181}
182
183pub(crate) fn get_transaction_inputs_outputs(
186 calls: &[ContractCall],
187 asset_inputs: Vec<Input>,
188 address: &Bech32Address,
189 base_asset_id: AssetId,
190) -> (Vec<Input>, Vec<Output>) {
191 let asset_ids = extract_unique_asset_ids(&asset_inputs, base_asset_id);
192 let contract_ids = extract_unique_contract_ids(calls);
193 let num_of_contracts = contract_ids.len();
194
195 let custom_inputs = calls.iter().flat_map(|c| c.inputs.clone()).collect_vec();
197 let custom_inputs_len = custom_inputs.len();
198 let custom_outputs = calls.iter().flat_map(|c| c.outputs.clone()).collect_vec();
199
200 let inputs = chain!(
201 custom_inputs,
202 generate_contract_inputs(contract_ids, custom_outputs.len()),
203 asset_inputs
204 )
205 .collect();
206
207 let outputs = chain!(
212 custom_outputs,
213 generate_contract_outputs(num_of_contracts, custom_inputs_len),
214 generate_asset_change_outputs(address, asset_ids),
215 generate_custom_outputs(calls),
216 )
217 .collect();
218
219 (inputs, outputs)
220}
221
222fn generate_custom_outputs(calls: &[ContractCall]) -> Vec<Output> {
223 calls
224 .iter()
225 .flat_map(|call| &call.custom_assets)
226 .group_by(|custom| (custom.0 .0, custom.0 .1.clone()))
227 .into_iter()
228 .filter_map(|(asset_id_address, groups_w_same_asset_id_address)| {
229 let total_amount_in_group = groups_w_same_asset_id_address
230 .map(|(_, amount)| amount)
231 .sum::<u64>();
232 match asset_id_address.1 {
233 Some(address) => Some(Output::coin(
234 address.into(),
235 total_amount_in_group,
236 asset_id_address.0,
237 )),
238 None => None,
239 }
240 })
241 .collect::<Vec<_>>()
242}
243
244fn extract_unique_asset_ids(asset_inputs: &[Input], base_asset_id: AssetId) -> HashSet<AssetId> {
245 asset_inputs
246 .iter()
247 .filter_map(|input| match input {
248 Input::ResourceSigned { resource, .. } | Input::ResourcePredicate { resource, .. } => {
249 Some(resource.coin_asset_id().unwrap_or(base_asset_id))
250 }
251 _ => None,
252 })
253 .collect()
254}
255
256fn generate_asset_change_outputs(
257 wallet_address: &Bech32Address,
258 asset_ids: HashSet<AssetId>,
259) -> Vec<Output> {
260 asset_ids
261 .into_iter()
262 .map(|asset_id| Output::change(wallet_address.into(), 0, asset_id))
263 .collect()
264}
265
266pub(crate) fn generate_contract_outputs(
268 num_of_contracts: usize,
269 num_current_inputs: usize,
270) -> Vec<Output> {
271 (0..num_of_contracts)
272 .map(|idx| {
273 Output::contract(
274 (idx + num_current_inputs) as u16,
275 Bytes32::zeroed(),
276 Bytes32::zeroed(),
277 )
278 })
279 .collect()
280}
281
282pub(crate) fn generate_contract_inputs(
284 contract_ids: HashSet<ContractId>,
285 num_current_outputs: usize,
286) -> Vec<Input> {
287 contract_ids
288 .into_iter()
289 .enumerate()
290 .map(|(idx, contract_id)| {
291 Input::contract(
292 UtxoId::new(Bytes32::zeroed(), (idx + num_current_outputs) as u16),
293 Bytes32::zeroed(),
294 Bytes32::zeroed(),
295 TxPointer::default(),
296 contract_id,
297 )
298 })
299 .collect()
300}
301
302fn extract_unique_contract_ids(calls: &[ContractCall]) -> HashSet<ContractId> {
303 calls
304 .iter()
305 .flat_map(|call| {
306 call.external_contracts
307 .iter()
308 .map(|bech32| bech32.into())
309 .chain(iter::once((&call.contract_id).into()))
310 })
311 .collect()
312}
313
314pub fn is_missing_output_variables(receipts: &[Receipt]) -> bool {
315 receipts.iter().any(
316 |r| matches!(r, Receipt::Revert { ra, .. } if *ra == FAILED_TRANSFER_TO_ADDRESS_SIGNAL),
317 )
318}
319
320pub fn find_id_of_missing_contract(receipts: &[Receipt]) -> Option<Bech32ContractId> {
321 receipts.iter().find_map(|receipt| match receipt {
322 Receipt::Panic {
323 reason,
324 contract_id,
325 ..
326 } if *reason.reason() == PanicReason::ContractNotInInputs => {
327 let contract_id = contract_id
328 .expect("panic caused by a contract not in inputs must have a contract id");
329 Some(Bech32ContractId::from(contract_id))
330 }
331 _ => None,
332 })
333}
334
335#[cfg(test)]
336mod test {
337 use std::slice;
338
339 use fuels_accounts::signers::private_key::PrivateKeySigner;
340 use fuels_core::types::{
341 coin::{Coin, CoinStatus},
342 coin_type::CoinType,
343 param_types::ParamType,
344 };
345 use rand::{thread_rng, Rng};
346
347 use super::*;
348 use crate::calls::{traits::ContractDependencyConfigurator, CallParameters};
349
350 fn new_contract_call_with_random_id() -> ContractCall {
351 ContractCall {
352 contract_id: random_bech32_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_bech32_contract_id() -> Bech32ContractId {
366 Bech32ContractId::new("fuel", rand::thread_rng().gen::<[u8; 32]>())
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.into(),
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.clone());
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.clone().into(),
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_bech32_contract_id();
446 let call = new_contract_call_with_random_id()
447 .with_external_contracts(vec![external_contract_id.clone()]);
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.into(), external_contract_id.into()].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_bech32_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 block_created: 0u32,
522 asset_id,
523 utxo_id: Default::default(),
524 owner: Default::default(),
525 status: CoinStatus::Unspent,
526 });
527 Input::resource_signed(coin)
528 })
529 .collect();
530 let call = new_contract_call_with_random_id();
531
532 let signer = PrivateKeySigner::random(&mut thread_rng());
533
534 let (_, outputs) =
536 get_transaction_inputs_outputs(&[call], coins, signer.address(), AssetId::zeroed());
537
538 let change_outputs: HashSet<Output> = outputs[1..].iter().cloned().collect();
540
541 let expected_change_outputs = asset_ids
542 .into_iter()
543 .map(|asset_id| Output::Change {
544 to: signer.address().into(),
545 amount: 0,
546 asset_id,
547 })
548 .collect();
549
550 assert_eq!(change_outputs, expected_change_outputs);
551 }
552
553 #[test]
554 fn will_collate_same_asset_ids() {
555 let asset_id_1 = AssetId::from([1; 32]);
556 let asset_id_2 = AssetId::from([2; 32]);
557
558 let calls = [
559 (asset_id_1, 100),
560 (asset_id_2, 200),
561 (asset_id_1, 300),
562 (asset_id_2, 400),
563 ]
564 .map(|(asset_id, amount)| {
565 CallParameters::default()
566 .with_amount(amount)
567 .with_asset_id(asset_id)
568 })
569 .map(|call_parameters| {
570 new_contract_call_with_random_id().with_call_parameters(call_parameters)
571 });
572
573 let asset_id_amounts = calculate_required_asset_amounts(&calls, AssetId::zeroed());
574
575 let expected_asset_id_amounts = [(asset_id_1, 400), (asset_id_2, 600)].into();
576
577 assert_eq!(
578 asset_id_amounts.into_iter().collect::<HashSet<_>>(),
579 expected_asset_id_amounts
580 )
581 }
582
583 mod compute_calls_instructions_len {
584 use fuel_asm::Instruction;
585 use fuels_core::types::param_types::{EnumVariants, ParamType};
586
587 use super::new_contract_call_with_random_id;
588 use crate::calls::utils::compute_calls_instructions_len;
589
590 const BASE_INSTRUCTION_COUNT: usize = 5;
592 const GAS_OFFSET_INSTRUCTION_COUNT: usize = 2;
594
595 #[test]
596 fn test_simple() {
597 let call = new_contract_call_with_random_id();
598 let instructions_len = compute_calls_instructions_len(&[call]);
599 assert_eq!(instructions_len, Instruction::SIZE * BASE_INSTRUCTION_COUNT);
600 }
601
602 #[test]
603 fn test_with_gas_offset() {
604 let mut call = new_contract_call_with_random_id();
605 call.call_parameters = call.call_parameters.with_gas_forwarded(0);
606 let instructions_len = compute_calls_instructions_len(&[call]);
607 assert_eq!(
608 instructions_len,
609 Instruction::SIZE * (BASE_INSTRUCTION_COUNT + GAS_OFFSET_INSTRUCTION_COUNT)
610 );
611 }
612
613 #[test]
614 fn test_with_enum_with_only_non_heap_variants() {
615 let mut call = new_contract_call_with_random_id();
616 call.output_param = ParamType::Enum {
617 name: "".to_string(),
618 enum_variants: EnumVariants::new(vec![
619 ("".to_string(), ParamType::Bool),
620 ("".to_string(), ParamType::U8),
621 ])
622 .unwrap(),
623 generics: Vec::new(),
624 };
625 let instructions_len = compute_calls_instructions_len(&[call]);
626 assert_eq!(
627 instructions_len,
628 Instruction::SIZE * BASE_INSTRUCTION_COUNT
630 );
631 }
632 }
633}