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_tx_from_contract_calls(
85 calls: &[ContractCall],
86 tx_policies: TxPolicies,
87 variable_outputs: VariableOutputPolicy,
88 account: &impl Account,
89) -> Result<ScriptTransaction> {
90 let mut tb =
91 transaction_builder_from_contract_calls(calls, tx_policies, variable_outputs, account)
92 .await?;
93
94 let consensus_parameters = account.try_provider()?.consensus_parameters().await?;
95 let base_asset_id = *consensus_parameters.base_asset_id();
96 let required_asset_amounts = calculate_required_asset_amounts(calls, base_asset_id);
97
98 let used_base_amount = required_asset_amounts
99 .iter()
100 .find_map(|(asset_id, amount)| (*asset_id == base_asset_id).then_some(*amount))
101 .unwrap_or_default();
102
103 account.add_witnesses(&mut tb)?;
104 account.adjust_for_fee(&mut tb, used_base_amount).await?;
105
106 tb.build(account.try_provider()?).await
107}
108
109fn compute_calls_instructions_len(calls: &[ContractCall]) -> usize {
112 calls
113 .iter()
114 .map(|c| {
115 let call_opcode_params = CallOpcodeParamsOffset {
119 gas_forwarded_offset: c.call_parameters.gas_forwarded().map(|_| 0),
120 ..CallOpcodeParamsOffset::default()
121 };
122
123 ContractCallInstructions::new(call_opcode_params)
124 .into_bytes()
125 .count()
126 })
127 .sum()
128}
129
130pub(crate) fn calculate_required_asset_amounts(
132 calls: &[ContractCall],
133 base_asset_id: AssetId,
134) -> Vec<(AssetId, u64)> {
135 let call_param_assets = calls.iter().map(|call| {
136 (
137 call.call_parameters.asset_id().unwrap_or(base_asset_id),
138 call.call_parameters.amount(),
139 )
140 });
141
142 let grouped_assets = calls
143 .iter()
144 .flat_map(|call| call.custom_assets.clone())
145 .map(|((asset_id, _), amount)| (asset_id, amount))
146 .chain(call_param_assets)
147 .sorted_by_key(|(asset_id, _)| *asset_id)
148 .group_by(|(asset_id, _)| *asset_id);
149
150 grouped_assets
151 .into_iter()
152 .filter_map(|(asset_id, groups_w_same_asset_id)| {
153 let total_amount_in_group = groups_w_same_asset_id.map(|(_, amount)| amount).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 inputs = chain!(generate_contract_inputs(contract_ids), asset_inputs).collect();
201
202 let outputs = chain!(
207 generate_contract_outputs(num_of_contracts),
208 generate_asset_change_outputs(address, asset_ids),
209 generate_custom_outputs(calls),
210 )
211 .collect();
212
213 (inputs, outputs)
214}
215
216fn generate_custom_outputs(calls: &[ContractCall]) -> Vec<Output> {
217 calls
218 .iter()
219 .flat_map(|call| &call.custom_assets)
220 .group_by(|custom| (custom.0 .0, custom.0 .1.clone()))
221 .into_iter()
222 .filter_map(|(asset_id_address, groups_w_same_asset_id_address)| {
223 let total_amount_in_group = groups_w_same_asset_id_address
224 .map(|(_, amount)| amount)
225 .sum::<u64>();
226 match asset_id_address.1 {
227 Some(address) => Some(Output::coin(
228 address.into(),
229 total_amount_in_group,
230 asset_id_address.0,
231 )),
232 None => None,
233 }
234 })
235 .collect::<Vec<_>>()
236}
237
238fn extract_unique_asset_ids(asset_inputs: &[Input], base_asset_id: AssetId) -> HashSet<AssetId> {
239 asset_inputs
240 .iter()
241 .filter_map(|input| match input {
242 Input::ResourceSigned { resource, .. } | Input::ResourcePredicate { resource, .. } => {
243 Some(resource.coin_asset_id().unwrap_or(base_asset_id))
244 }
245 _ => None,
246 })
247 .collect()
248}
249
250fn generate_asset_change_outputs(
251 wallet_address: &Bech32Address,
252 asset_ids: HashSet<AssetId>,
253) -> Vec<Output> {
254 asset_ids
255 .into_iter()
256 .map(|asset_id| Output::change(wallet_address.into(), 0, asset_id))
257 .collect()
258}
259
260pub(crate) fn generate_contract_outputs(num_of_contracts: usize) -> Vec<Output> {
261 (0..num_of_contracts)
262 .map(|idx| Output::contract(idx as u16, Bytes32::zeroed(), Bytes32::zeroed()))
263 .collect()
264}
265
266pub(crate) fn generate_contract_inputs(contract_ids: HashSet<ContractId>) -> Vec<Input> {
267 contract_ids
268 .into_iter()
269 .enumerate()
270 .map(|(idx, contract_id)| {
271 Input::contract(
272 UtxoId::new(Bytes32::zeroed(), idx as u16),
273 Bytes32::zeroed(),
274 Bytes32::zeroed(),
275 TxPointer::default(),
276 contract_id,
277 )
278 })
279 .collect()
280}
281
282fn extract_unique_contract_ids(calls: &[ContractCall]) -> HashSet<ContractId> {
283 calls
284 .iter()
285 .flat_map(|call| {
286 call.external_contracts
287 .iter()
288 .map(|bech32| bech32.into())
289 .chain(iter::once((&call.contract_id).into()))
290 })
291 .collect()
292}
293
294pub fn is_missing_output_variables(receipts: &[Receipt]) -> bool {
295 receipts.iter().any(
296 |r| matches!(r, Receipt::Revert { ra, .. } if *ra == FAILED_TRANSFER_TO_ADDRESS_SIGNAL),
297 )
298}
299
300pub fn find_id_of_missing_contract(receipts: &[Receipt]) -> Option<Bech32ContractId> {
301 receipts.iter().find_map(|receipt| match receipt {
302 Receipt::Panic {
303 reason,
304 contract_id,
305 ..
306 } if *reason.reason() == PanicReason::ContractNotInInputs => {
307 let contract_id = contract_id
308 .expect("panic caused by a contract not in inputs must have a contract id");
309 Some(Bech32ContractId::from(contract_id))
310 }
311 _ => None,
312 })
313}
314
315#[cfg(test)]
316mod test {
317 use std::slice;
318
319 use fuels_accounts::wallet::WalletUnlocked;
320 use fuels_core::types::{
321 coin::{Coin, CoinStatus},
322 coin_type::CoinType,
323 param_types::ParamType,
324 };
325 use rand::Rng;
326
327 use super::*;
328 use crate::calls::{traits::ContractDependencyConfigurator, CallParameters};
329
330 fn new_contract_call_with_random_id() -> ContractCall {
331 ContractCall {
332 contract_id: random_bech32_contract_id(),
333 encoded_args: Ok(Default::default()),
334 encoded_selector: [0; 8].to_vec(),
335 call_parameters: Default::default(),
336 external_contracts: Default::default(),
337 output_param: ParamType::Unit,
338 is_payable: false,
339 custom_assets: Default::default(),
340 }
341 }
342
343 fn random_bech32_contract_id() -> Bech32ContractId {
344 Bech32ContractId::new("fuel", rand::thread_rng().gen::<[u8; 32]>())
345 }
346
347 #[test]
348 fn contract_input_present() {
349 let call = new_contract_call_with_random_id();
350
351 let wallet = WalletUnlocked::new_random(None);
352
353 let (inputs, _) = get_transaction_inputs_outputs(
354 slice::from_ref(&call),
355 Default::default(),
356 wallet.address(),
357 AssetId::zeroed(),
358 );
359
360 assert_eq!(
361 inputs,
362 vec![Input::contract(
363 UtxoId::new(Bytes32::zeroed(), 0),
364 Bytes32::zeroed(),
365 Bytes32::zeroed(),
366 TxPointer::default(),
367 call.contract_id.into(),
368 )]
369 );
370 }
371
372 #[test]
373 fn contract_input_is_not_duplicated() {
374 let call = new_contract_call_with_random_id();
375 let call_w_same_contract =
376 new_contract_call_with_random_id().with_contract_id(call.contract_id.clone());
377
378 let wallet = WalletUnlocked::new_random(None);
379
380 let calls = [call, call_w_same_contract];
381
382 let (inputs, _) = get_transaction_inputs_outputs(
383 &calls,
384 Default::default(),
385 wallet.address(),
386 AssetId::zeroed(),
387 );
388
389 assert_eq!(
390 inputs,
391 vec![Input::contract(
392 UtxoId::new(Bytes32::zeroed(), 0),
393 Bytes32::zeroed(),
394 Bytes32::zeroed(),
395 TxPointer::default(),
396 calls[0].contract_id.clone().into(),
397 )]
398 );
399 }
400
401 #[test]
402 fn contract_output_present() {
403 let call = new_contract_call_with_random_id();
404
405 let wallet = WalletUnlocked::new_random(None);
406
407 let (_, outputs) = get_transaction_inputs_outputs(
408 &[call],
409 Default::default(),
410 wallet.address(),
411 AssetId::zeroed(),
412 );
413
414 assert_eq!(
415 outputs,
416 vec![Output::contract(0, Bytes32::zeroed(), Bytes32::zeroed())]
417 );
418 }
419
420 #[test]
421 fn external_contract_input_present() {
422 let external_contract_id = random_bech32_contract_id();
424 let call = new_contract_call_with_random_id()
425 .with_external_contracts(vec![external_contract_id.clone()]);
426
427 let wallet = WalletUnlocked::new_random(None);
428
429 let (inputs, _) = get_transaction_inputs_outputs(
431 slice::from_ref(&call),
432 Default::default(),
433 wallet.address(),
434 AssetId::zeroed(),
435 );
436
437 let mut expected_contract_ids: HashSet<ContractId> =
439 [call.contract_id.into(), external_contract_id.into()].into();
440
441 for (index, input) in inputs.into_iter().enumerate() {
442 match input {
443 Input::Contract {
444 utxo_id,
445 balance_root,
446 state_root,
447 tx_pointer,
448 contract_id,
449 } => {
450 assert_eq!(utxo_id, UtxoId::new(Bytes32::zeroed(), index as u16));
451 assert_eq!(balance_root, Bytes32::zeroed());
452 assert_eq!(state_root, Bytes32::zeroed());
453 assert_eq!(tx_pointer, TxPointer::default());
454 assert!(expected_contract_ids.contains(&contract_id));
455 expected_contract_ids.remove(&contract_id);
456 }
457 _ => {
458 panic!("expected only inputs of type `Input::Contract`");
459 }
460 }
461 }
462 }
463
464 #[test]
465 fn external_contract_output_present() {
466 let external_contract_id = random_bech32_contract_id();
468 let call =
469 new_contract_call_with_random_id().with_external_contracts(vec![external_contract_id]);
470
471 let wallet = WalletUnlocked::new_random(None);
472
473 let (_, outputs) = get_transaction_inputs_outputs(
475 &[call],
476 Default::default(),
477 wallet.address(),
478 AssetId::zeroed(),
479 );
480
481 let expected_outputs = (0..=1)
483 .map(|i| Output::contract(i, Bytes32::zeroed(), Bytes32::zeroed()))
484 .collect::<Vec<_>>();
485
486 assert_eq!(outputs, expected_outputs);
487 }
488
489 #[test]
490 fn change_per_asset_id_added() {
491 let asset_ids = [AssetId::zeroed(), AssetId::from([1; 32])];
493
494 let coins = asset_ids
495 .into_iter()
496 .map(|asset_id| {
497 let coin = CoinType::Coin(Coin {
498 amount: 100,
499 block_created: 0u32,
500 asset_id,
501 utxo_id: Default::default(),
502 owner: Default::default(),
503 status: CoinStatus::Unspent,
504 });
505 Input::resource_signed(coin)
506 })
507 .collect();
508 let call = new_contract_call_with_random_id();
509
510 let wallet = WalletUnlocked::new_random(None);
511
512 let (_, outputs) =
514 get_transaction_inputs_outputs(&[call], coins, wallet.address(), AssetId::zeroed());
515
516 let change_outputs: HashSet<Output> = outputs[1..].iter().cloned().collect();
518
519 let expected_change_outputs = asset_ids
520 .into_iter()
521 .map(|asset_id| Output::Change {
522 to: wallet.address().into(),
523 amount: 0,
524 asset_id,
525 })
526 .collect();
527
528 assert_eq!(change_outputs, expected_change_outputs);
529 }
530
531 #[test]
532 fn will_collate_same_asset_ids() {
533 let asset_id_1 = AssetId::from([1; 32]);
534 let asset_id_2 = AssetId::from([2; 32]);
535
536 let calls = [
537 (asset_id_1, 100),
538 (asset_id_2, 200),
539 (asset_id_1, 300),
540 (asset_id_2, 400),
541 ]
542 .map(|(asset_id, amount)| {
543 CallParameters::default()
544 .with_amount(amount)
545 .with_asset_id(asset_id)
546 })
547 .map(|call_parameters| {
548 new_contract_call_with_random_id().with_call_parameters(call_parameters)
549 });
550
551 let asset_id_amounts = calculate_required_asset_amounts(&calls, AssetId::zeroed());
552
553 let expected_asset_id_amounts = [(asset_id_1, 400), (asset_id_2, 600)].into();
554
555 assert_eq!(
556 asset_id_amounts.into_iter().collect::<HashSet<_>>(),
557 expected_asset_id_amounts
558 )
559 }
560
561 mod compute_calls_instructions_len {
562 use fuel_asm::Instruction;
563 use fuels_core::types::param_types::{EnumVariants, ParamType};
564
565 use super::new_contract_call_with_random_id;
566 use crate::calls::utils::compute_calls_instructions_len;
567
568 const BASE_INSTRUCTION_COUNT: usize = 5;
570 const GAS_OFFSET_INSTRUCTION_COUNT: usize = 2;
572
573 #[test]
574 fn test_simple() {
575 let call = new_contract_call_with_random_id();
576 let instructions_len = compute_calls_instructions_len(&[call]);
577 assert_eq!(instructions_len, Instruction::SIZE * BASE_INSTRUCTION_COUNT);
578 }
579
580 #[test]
581 fn test_with_gas_offset() {
582 let mut call = new_contract_call_with_random_id();
583 call.call_parameters = call.call_parameters.with_gas_forwarded(0);
584 let instructions_len = compute_calls_instructions_len(&[call]);
585 assert_eq!(
586 instructions_len,
587 Instruction::SIZE * (BASE_INSTRUCTION_COUNT + GAS_OFFSET_INSTRUCTION_COUNT)
588 );
589 }
590
591 #[test]
592 fn test_with_enum_with_only_non_heap_variants() {
593 let mut call = new_contract_call_with_random_id();
594 call.output_param = ParamType::Enum {
595 name: "".to_string(),
596 enum_variants: EnumVariants::new(vec![
597 ("".to_string(), ParamType::Bool),
598 ("".to_string(), ParamType::U8),
599 ])
600 .unwrap(),
601 generics: Vec::new(),
602 };
603 let instructions_len = compute_calls_instructions_len(&[call]);
604 assert_eq!(
605 instructions_len,
606 Instruction::SIZE * BASE_INSTRUCTION_COUNT
608 );
609 }
610 }
611}