1use fuel_gql_client::fuel_tx::{
2 field::Script as ScriptField, ConsensusParameters, Input, Output, TxPointer, UtxoId,
3};
4use fuel_gql_client::fuel_types::{bytes::padded_len_usize, Immediate18, Word};
5use fuel_gql_client::fuel_vm::{consts::REG_ONE, prelude::Opcode};
6use fuel_tx::{AssetId, Bytes32, ContractId};
7use fuels_core::constants::BASE_ASSET_ID;
8use fuels_types::bech32::Bech32Address;
9use fuels_types::constants::WORD_SIZE;
10use fuels_types::resource::Resource;
11use itertools::{chain, Itertools};
12use std::collections::HashSet;
13use std::{iter, vec};
14
15use crate::contract::ContractCall;
16
17#[derive(Default)]
18pub(crate) struct CallOpcodeParamsOffset {
21 pub asset_id_offset: usize,
22 pub amount_offset: usize,
23 pub gas_forwarded_offset: usize,
24 pub call_data_offset: usize,
25}
26
27pub(crate) fn calculate_required_asset_amounts(calls: &[ContractCall]) -> Vec<(AssetId, u64)> {
29 let amounts_per_asset_id = calls
30 .iter()
31 .map(|call| (call.call_parameters.asset_id, call.call_parameters.amount))
32 .collect::<Vec<_>>();
33 sum_up_amounts_for_each_asset_id(amounts_per_asset_id)
34}
35
36fn sum_up_amounts_for_each_asset_id(
39 amounts_per_asset_id: Vec<(AssetId, u64)>,
40) -> Vec<(AssetId, u64)> {
41 amounts_per_asset_id
42 .into_iter()
43 .sorted_by_key(|(asset_id, _)| *asset_id)
44 .group_by(|(asset_id, _)| *asset_id)
45 .into_iter()
46 .map(|(asset_id, groups_w_same_asset_id)| {
47 let total_amount_in_group = groups_w_same_asset_id.map(|(_, amount)| amount).sum();
48 (asset_id, total_amount_in_group)
49 })
50 .collect()
51}
52
53pub(crate) fn get_instructions(
55 calls: &[ContractCall],
56 offsets: Vec<CallOpcodeParamsOffset>,
57) -> Vec<u8> {
58 let num_calls = calls.len();
59
60 let mut instructions = vec![];
61 for (_, call_offsets) in (0..num_calls).zip(offsets.iter()) {
62 instructions.extend(get_single_call_instructions(call_offsets));
63 }
64
65 instructions.extend(Opcode::RET(REG_ONE).to_bytes());
66
67 instructions
68}
69
70pub(crate) fn build_script_data_from_contract_calls(
79 calls: &[ContractCall],
80 data_offset: usize,
81 gas_limit: u64,
82) -> (Vec<u8>, Vec<CallOpcodeParamsOffset>) {
83 let mut script_data = vec![];
84 let mut param_offsets = vec![];
85
86 let mut segment_offset = data_offset;
88
89 for call in calls {
90 let call_param_offsets = CallOpcodeParamsOffset {
91 asset_id_offset: segment_offset,
92 amount_offset: segment_offset + AssetId::LEN,
93 gas_forwarded_offset: segment_offset + AssetId::LEN + WORD_SIZE,
94 call_data_offset: segment_offset + AssetId::LEN + 2 * WORD_SIZE,
95 };
96 param_offsets.push(call_param_offsets);
97
98 script_data.extend(call.call_parameters.asset_id.to_vec());
99
100 script_data.extend(call.call_parameters.amount.to_be_bytes());
101
102 let gas_forwarded = call.call_parameters.gas_forwarded.unwrap_or(gas_limit);
104 script_data.extend(gas_forwarded.to_be_bytes());
105
106 script_data.extend(call.contract_id.hash().as_ref());
107
108 script_data.extend(call.encoded_selector);
109
110 let encoded_args_start_offset = if call.compute_custom_input_offset {
115 let custom_input_offset =
118 segment_offset + AssetId::LEN + 2 * WORD_SIZE + ContractId::LEN + 2 * WORD_SIZE;
119 script_data.extend((custom_input_offset as Word).to_be_bytes());
120 custom_input_offset
121 } else {
122 segment_offset
123 };
124
125 let bytes = call.encoded_args.resolve(encoded_args_start_offset as u64);
126 script_data.extend(bytes);
127
128 segment_offset = data_offset + script_data.len();
131 }
132
133 (script_data, param_offsets)
134}
135
136fn get_single_call_instructions(offsets: &CallOpcodeParamsOffset) -> Vec<u8> {
148 let instructions = vec![
149 Opcode::MOVI(0x10, offsets.call_data_offset as Immediate18),
150 Opcode::MOVI(0x11, offsets.gas_forwarded_offset as Immediate18),
151 Opcode::LW(0x11, 0x11, 0),
152 Opcode::MOVI(0x12, offsets.amount_offset as Immediate18),
153 Opcode::LW(0x12, 0x12, 0),
154 Opcode::MOVI(0x13, offsets.asset_id_offset as Immediate18),
155 Opcode::CALL(0x10, 0x12, 0x13, 0x11),
156 ];
157
158 #[allow(clippy::iter_cloned_collect)]
159 instructions.iter().copied().collect::<Vec<u8>>()
160}
161
162pub(crate) fn get_transaction_inputs_outputs(
165 calls: &[ContractCall],
166 wallet_address: &Bech32Address,
167 spendable_resources: Vec<Resource>,
168) -> (Vec<Input>, Vec<Output>) {
169 let asset_ids = extract_unique_asset_ids(&spendable_resources);
170 let contract_ids = extract_unique_contract_ids(calls);
171 let num_of_contracts = contract_ids.len();
172
173 let inputs = chain!(
174 generate_contract_inputs(contract_ids),
175 convert_to_signed_resources(spendable_resources),
176 )
177 .collect();
178
179 let outputs = chain!(
184 generate_contract_outputs(num_of_contracts),
185 generate_asset_change_outputs(wallet_address, asset_ids),
186 extract_variable_outputs(calls),
187 extract_message_outputs(calls)
188 )
189 .collect();
190 (inputs, outputs)
191}
192
193fn extract_unique_asset_ids(spendable_coins: &[Resource]) -> HashSet<AssetId> {
194 spendable_coins
195 .iter()
196 .map(|resource| match resource {
197 Resource::Coin(coin) => coin.asset_id,
198 Resource::Message(_) => BASE_ASSET_ID,
199 })
200 .collect()
201}
202
203fn extract_variable_outputs(calls: &[ContractCall]) -> Vec<Output> {
204 calls
205 .iter()
206 .filter_map(|call| call.variable_outputs.clone())
207 .flatten()
208 .collect()
209}
210
211fn extract_message_outputs(calls: &[ContractCall]) -> Vec<Output> {
212 calls
213 .iter()
214 .filter_map(|call| call.message_outputs.clone())
215 .flatten()
216 .collect()
217}
218
219fn generate_asset_change_outputs(
220 wallet_address: &Bech32Address,
221 asset_ids: HashSet<AssetId>,
222) -> Vec<Output> {
223 asset_ids
224 .into_iter()
225 .map(|asset_id| Output::change(wallet_address.into(), 0, asset_id))
226 .collect()
227}
228
229fn generate_contract_outputs(num_of_contracts: usize) -> Vec<Output> {
230 (0..num_of_contracts)
231 .map(|idx| Output::contract(idx as u8, Bytes32::zeroed(), Bytes32::zeroed()))
232 .collect()
233}
234
235fn convert_to_signed_resources(spendable_resources: Vec<Resource>) -> Vec<Input> {
236 spendable_resources
237 .into_iter()
238 .map(|resource| match resource {
239 Resource::Coin(coin) => Input::coin_signed(
240 coin.utxo_id,
241 coin.owner.into(),
242 coin.amount,
243 coin.asset_id,
244 TxPointer::default(),
245 0,
246 coin.maturity,
247 ),
248 Resource::Message(message) => Input::message_signed(
249 message.message_id(),
250 message.sender.into(),
251 message.recipient.into(),
252 message.amount,
253 message.nonce,
254 0,
255 message.data,
256 ),
257 })
258 .collect()
259}
260
261pub fn get_base_script_offset(consensus_parameters: &ConsensusParameters) -> usize {
264 consensus_parameters.tx_offset() + fuel_tx::Script::script_offset_static()
265}
266
267pub(crate) fn get_data_offset(
270 consensus_parameters: &ConsensusParameters,
271 num_calls: usize,
272) -> usize {
273 let len_script =
275 get_single_call_instructions(&CallOpcodeParamsOffset::default()).len() * num_calls;
276
277 let opcode_len = Opcode::LEN;
279
280 get_base_script_offset(consensus_parameters) + padded_len_usize(len_script + opcode_len)
281}
282
283fn generate_contract_inputs(contract_ids: HashSet<ContractId>) -> Vec<Input> {
284 contract_ids
285 .into_iter()
286 .enumerate()
287 .map(|(idx, contract_id)| {
288 Input::contract(
289 UtxoId::new(Bytes32::zeroed(), idx as u8),
290 Bytes32::zeroed(),
291 Bytes32::zeroed(),
292 TxPointer::default(),
293 contract_id,
294 )
295 })
296 .collect()
297}
298
299fn extract_unique_contract_ids(calls: &[ContractCall]) -> HashSet<ContractId> {
300 calls
301 .iter()
302 .flat_map(|call| {
303 call.external_contracts
304 .iter()
305 .map(|bech32| bech32.into())
306 .chain(iter::once((&call.contract_id).into()))
307 })
308 .collect()
309}
310
311#[cfg(test)]
312mod test {
313 use super::*;
314 use fuels_core::abi_encoder::ABIEncoder;
315 use fuels_core::parameters::CallParameters;
316 use fuels_core::Token;
317 use fuels_types::bech32::Bech32ContractId;
318 use fuels_types::coin::{Coin, CoinStatus};
319 use fuels_types::param_types::ParamType;
320 use rand::Rng;
321 use std::slice;
322
323 impl ContractCall {
324 pub fn new_with_random_id() -> Self {
325 ContractCall {
326 contract_id: random_bech32_contract_id(),
327 encoded_args: Default::default(),
328 encoded_selector: [0; 8],
329 call_parameters: Default::default(),
330 compute_custom_input_offset: false,
331 variable_outputs: None,
332 external_contracts: Default::default(),
333 output_param: ParamType::Unit,
334 message_outputs: None,
335 }
336 }
337 }
338
339 fn random_bech32_addr() -> Bech32Address {
340 Bech32Address::new("fuel", rand::thread_rng().gen::<[u8; 32]>())
341 }
342
343 fn random_bech32_contract_id() -> Bech32ContractId {
344 Bech32ContractId::new("fuel", rand::thread_rng().gen::<[u8; 32]>())
345 }
346
347 #[tokio::test]
348 async fn test_script_data() {
349 const SELECTOR_LEN: usize = WORD_SIZE;
351 const NUM_CALLS: usize = 3;
352
353 let contract_ids = vec![
354 Bech32ContractId::new("test", Bytes32::new([1u8; 32])),
355 Bech32ContractId::new("test", Bytes32::new([1u8; 32])),
356 Bech32ContractId::new("test", Bytes32::new([1u8; 32])),
357 ];
358
359 let asset_ids = vec![
360 AssetId::from([4u8; 32]),
361 AssetId::from([5u8; 32]),
362 AssetId::from([6u8; 32]),
363 ];
364
365 let selectors = vec![[7u8; 8], [8u8; 8], [9u8; 8]];
366
367 let args = [Token::U8(1), Token::U16(2), Token::U8(3)]
370 .map(|token| ABIEncoder::encode(&[token]).unwrap())
371 .to_vec();
372
373 let calls: Vec<ContractCall> = (0..NUM_CALLS)
374 .map(|i| ContractCall {
375 contract_id: contract_ids[i].clone(),
376 encoded_selector: selectors[i],
377 encoded_args: args[i].clone(),
378 call_parameters: CallParameters::new(
379 Some(i as u64),
380 Some(asset_ids[i]),
381 Some(i as u64),
382 ),
383 compute_custom_input_offset: i == 1,
384 variable_outputs: None,
385 message_outputs: None,
386 external_contracts: vec![],
387 output_param: ParamType::Unit,
388 })
389 .collect();
390
391 let (script_data, param_offsets) = build_script_data_from_contract_calls(&calls, 0, 0);
393
394 assert_eq!(param_offsets.len(), NUM_CALLS);
396 for (idx, offsets) in param_offsets.iter().enumerate() {
397 let asset_id = script_data
398 [offsets.asset_id_offset..offsets.asset_id_offset + AssetId::LEN]
399 .to_vec();
400 assert_eq!(asset_id, asset_ids[idx].to_vec());
401
402 let amount =
403 script_data[offsets.amount_offset..offsets.amount_offset + WORD_SIZE].to_vec();
404 assert_eq!(amount, idx.to_be_bytes());
405
406 let gas = script_data
407 [offsets.gas_forwarded_offset..offsets.gas_forwarded_offset + WORD_SIZE]
408 .to_vec();
409 assert_eq!(gas, idx.to_be_bytes().to_vec());
410
411 let contract_id =
412 &script_data[offsets.call_data_offset..offsets.call_data_offset + ContractId::LEN];
413 let expected_contract_id = contract_ids[idx].hash();
414 assert_eq!(contract_id, expected_contract_id.as_slice());
415
416 let selector_offset = offsets.call_data_offset + ContractId::LEN;
417 let selector = script_data[selector_offset..selector_offset + SELECTOR_LEN].to_vec();
418 assert_eq!(selector, selectors[idx].to_vec());
419 }
420
421 let call_1_arg_offset = param_offsets[0].call_data_offset + ContractId::LEN + SELECTOR_LEN;
423 let call_1_arg = script_data[call_1_arg_offset..call_1_arg_offset + WORD_SIZE].to_vec();
424 assert_eq!(call_1_arg, args[0].resolve(0));
425
426 let call_3_arg_offset = param_offsets[2].call_data_offset + ContractId::LEN + SELECTOR_LEN;
427 let call_3_arg = script_data[call_3_arg_offset..call_3_arg_offset + WORD_SIZE].to_vec();
428 assert_eq!(call_3_arg, args[2].resolve(0));
429
430 let call_2_arg_offset = param_offsets[1].call_data_offset + ContractId::LEN + SELECTOR_LEN;
432 let custom_input_offset =
433 script_data[call_2_arg_offset..call_2_arg_offset + WORD_SIZE].to_vec();
434 assert_eq!(
435 custom_input_offset,
436 (call_2_arg_offset + WORD_SIZE).to_be_bytes()
437 );
438
439 let custom_input_offset =
440 param_offsets[1].call_data_offset + ContractId::LEN + SELECTOR_LEN + WORD_SIZE;
441 let custom_input =
442 script_data[custom_input_offset..custom_input_offset + WORD_SIZE].to_vec();
443 assert_eq!(custom_input, args[1].resolve(0));
444 }
445
446 #[test]
447 fn contract_input_present() {
448 let call = ContractCall::new_with_random_id();
449
450 let (inputs, _) = get_transaction_inputs_outputs(
451 slice::from_ref(&call),
452 &random_bech32_addr(),
453 Default::default(),
454 );
455
456 assert_eq!(
457 inputs,
458 vec![Input::contract(
459 UtxoId::new(Bytes32::zeroed(), 0),
460 Bytes32::zeroed(),
461 Bytes32::zeroed(),
462 TxPointer::default(),
463 call.contract_id.into(),
464 )]
465 );
466 }
467
468 #[test]
469 fn contract_input_is_not_duplicated() {
470 let call = ContractCall::new_with_random_id();
471 let call_w_same_contract =
472 ContractCall::new_with_random_id().with_contract_id(call.contract_id.clone());
473
474 let calls = [call, call_w_same_contract];
475
476 let (inputs, _) =
477 get_transaction_inputs_outputs(&calls, &random_bech32_addr(), Default::default());
478
479 assert_eq!(
480 inputs,
481 vec![Input::contract(
482 UtxoId::new(Bytes32::zeroed(), 0),
483 Bytes32::zeroed(),
484 Bytes32::zeroed(),
485 TxPointer::default(),
486 calls[0].contract_id.clone().into(),
487 )]
488 );
489 }
490
491 #[test]
492 fn contract_output_present() {
493 let call = ContractCall::new_with_random_id();
494
495 let (_, outputs) =
496 get_transaction_inputs_outputs(&[call], &random_bech32_addr(), Default::default());
497
498 assert_eq!(
499 outputs,
500 vec![Output::contract(0, Bytes32::zeroed(), Bytes32::zeroed())]
501 );
502 }
503
504 #[test]
505 fn external_contract_input_present() {
506 let external_contract_id = random_bech32_contract_id();
508 let call = ContractCall::new_with_random_id()
509 .with_external_contracts(vec![external_contract_id.clone()]);
510
511 let (inputs, _) = get_transaction_inputs_outputs(
513 slice::from_ref(&call),
514 &random_bech32_addr(),
515 Default::default(),
516 );
517
518 let mut expected_contract_ids: HashSet<ContractId> =
520 [call.contract_id.into(), external_contract_id.into()].into();
521
522 for (index, input) in inputs.into_iter().enumerate() {
523 match input {
524 Input::Contract {
525 utxo_id,
526 balance_root,
527 state_root,
528 tx_pointer,
529 contract_id,
530 } => {
531 assert_eq!(utxo_id, UtxoId::new(Bytes32::zeroed(), index as u8));
532 assert_eq!(balance_root, Bytes32::zeroed());
533 assert_eq!(state_root, Bytes32::zeroed());
534 assert_eq!(tx_pointer, TxPointer::default());
535 assert!(expected_contract_ids.contains(&contract_id));
536 expected_contract_ids.remove(&contract_id);
537 }
538 _ => {
539 panic!("Expected only inputs of type Input::Contract");
540 }
541 }
542 }
543 }
544
545 #[test]
546 fn external_contract_output_present() {
547 let external_contract_id = random_bech32_contract_id();
549 let call =
550 ContractCall::new_with_random_id().with_external_contracts(vec![external_contract_id]);
551
552 let (_, outputs) =
554 get_transaction_inputs_outputs(&[call], &random_bech32_addr(), Default::default());
555
556 let expected_outputs = (0..=1)
558 .map(|i| Output::contract(i, Bytes32::zeroed(), Bytes32::zeroed()))
559 .collect::<Vec<_>>();
560
561 assert_eq!(outputs, expected_outputs);
562 }
563
564 #[test]
565 fn change_per_asset_id_added() {
566 let wallet_addr = random_bech32_addr();
568 let asset_ids = [AssetId::default(), AssetId::from([1; 32])];
569
570 let coins = asset_ids
571 .into_iter()
572 .map(|asset_id| {
573 Resource::Coin(Coin {
574 amount: 100,
575 block_created: 0,
576 asset_id,
577 utxo_id: Default::default(),
578 maturity: 0,
579 owner: Default::default(),
580 status: CoinStatus::Unspent,
581 })
582 })
583 .collect();
584 let call = ContractCall::new_with_random_id();
585
586 let (_, outputs) = get_transaction_inputs_outputs(&[call], &wallet_addr, coins);
588
589 let change_outputs: HashSet<Output> = outputs[1..].iter().cloned().collect();
591
592 let expected_change_outputs = asset_ids
593 .into_iter()
594 .map(|asset_id| Output::Change {
595 to: wallet_addr.clone().into(),
596 amount: 0,
597 asset_id,
598 })
599 .collect();
600
601 assert_eq!(change_outputs, expected_change_outputs);
602 }
603
604 #[test]
605 fn spendable_coins_added_to_input() {
606 let asset_ids = [AssetId::default(), AssetId::from([1; 32])];
608
609 let generate_spendable_resources = || {
610 asset_ids
611 .into_iter()
612 .enumerate()
613 .map(|(index, asset_id)| {
614 Resource::Coin(Coin {
615 amount: (index * 10) as u64,
616 block_created: 1,
617 asset_id,
618 utxo_id: Default::default(),
619 maturity: 0,
620 owner: Default::default(),
621 status: CoinStatus::Unspent,
622 })
623 })
624 .collect::<Vec<_>>()
625 };
626
627 let call = ContractCall::new_with_random_id();
628
629 let (inputs, _) = get_transaction_inputs_outputs(
631 &[call],
632 &random_bech32_addr(),
633 generate_spendable_resources(),
634 );
635
636 let inputs_as_signed_coins: HashSet<Input> = inputs[1..].iter().cloned().collect();
638
639 let expected_inputs = generate_spendable_resources()
640 .into_iter()
641 .map(|resource| match resource {
642 Resource::Coin(coin) => Input::coin_signed(
643 coin.utxo_id,
644 coin.owner.into(),
645 coin.amount,
646 coin.asset_id,
647 TxPointer::default(),
648 0,
649 0,
650 ),
651 Resource::Message(_) => panic!("Resources contained messages."),
652 })
653 .collect::<HashSet<_>>();
654
655 assert_eq!(expected_inputs, inputs_as_signed_coins);
656 }
657
658 #[test]
659 fn variable_outputs_appended_to_outputs() {
660 let variable_outputs = [100, 200].map(|amount| {
662 Output::variable(random_bech32_addr().into(), amount, Default::default())
663 });
664
665 let calls = variable_outputs
666 .iter()
667 .cloned()
668 .map(|variable_output| {
669 ContractCall::new_with_random_id().with_variable_outputs(vec![variable_output])
670 })
671 .collect::<Vec<_>>();
672
673 let (_, outputs) =
675 get_transaction_inputs_outputs(&calls, &random_bech32_addr(), Default::default());
676
677 let actual_variable_outputs: HashSet<Output> = outputs[2..].iter().cloned().collect();
679 let expected_outputs: HashSet<Output> = variable_outputs.into();
680
681 assert_eq!(expected_outputs, actual_variable_outputs);
682 }
683
684 #[test]
685 fn message_outputs_appended_to_outputs() {
686 let message_outputs =
688 [100, 200].map(|amount| Output::message(random_bech32_addr().into(), amount));
689
690 let calls = message_outputs
691 .iter()
692 .cloned()
693 .map(|message_output| {
694 ContractCall::new_with_random_id().with_message_outputs(vec![message_output])
695 })
696 .collect::<Vec<_>>();
697
698 let (_, outputs) =
700 get_transaction_inputs_outputs(&calls, &random_bech32_addr(), Default::default());
701
702 let actual_message_outputs: HashSet<Output> = outputs[2..].iter().cloned().collect();
704 let expected_outputs: HashSet<Output> = message_outputs.into();
705
706 assert_eq!(expected_outputs, actual_message_outputs);
707 }
708
709 #[test]
710 fn will_collate_same_asset_ids() {
711 let asset_id_1 = AssetId::from([1; 32]);
712 let asset_id_2 = AssetId::from([2; 32]);
713
714 let calls = [
715 (asset_id_1, 100),
716 (asset_id_2, 200),
717 (asset_id_1, 300),
718 (asset_id_2, 400),
719 ]
720 .map(|(asset_id, amount)| CallParameters::new(Some(amount), Some(asset_id), None))
721 .map(|call_parameters| {
722 ContractCall::new_with_random_id().with_call_parameters(call_parameters)
723 });
724
725 let asset_id_amounts = calculate_required_asset_amounts(&calls);
726
727 let expected_asset_id_amounts = [(asset_id_1, 400), (asset_id_2, 600)].into();
728
729 assert_eq!(
730 asset_id_amounts.into_iter().collect::<HashSet<_>>(),
731 expected_asset_id_amounts
732 )
733 }
734}