fuels_contract/
contract.rs

1use crate::{
2    call_response::FuelCallResponse,
3    execution_script::ExecutableFuelCall,
4    logs::{decode_revert_error, LogDecoder},
5};
6use fuel_gql_client::{
7    fuel_tx::{Contract as FuelContract, Output, Receipt, StorageSlot, Transaction},
8    prelude::PanicReason,
9};
10use fuel_tx::{Address, AssetId, Checkable, Create, Salt};
11use fuels_core::{
12    abi_decoder::ABIDecoder,
13    abi_encoder::{ABIEncoder, UnresolvedBytes},
14    constants::FAILED_TRANSFER_TO_ADDRESS_SIGNAL,
15    parameters::StorageConfiguration,
16    parameters::{CallParameters, TxParameters},
17    tx::{Bytes32, ContractId},
18    Parameterize, Selector, Token, Tokenizable,
19};
20use fuels_signers::{
21    provider::{Provider, TransactionCost},
22    Signer, WalletUnlocked,
23};
24use fuels_types::{
25    bech32::Bech32ContractId,
26    errors::Error,
27    param_types::{ParamType, ReturnLocation},
28};
29use std::{
30    collections::{HashMap, HashSet},
31    fmt::Debug,
32    fs,
33    marker::PhantomData,
34    panic,
35    path::Path,
36    str::FromStr,
37};
38
39/// How many times to attempt to resolve missing tx dependencies.
40pub const DEFAULT_TX_DEP_ESTIMATION_ATTEMPTS: u64 = 10;
41
42/// A compiled representation of a contract.
43#[derive(Debug, Clone, Default)]
44pub struct CompiledContract {
45    pub raw: Vec<u8>,
46    pub salt: Salt,
47    pub storage_slots: Vec<StorageSlot>,
48}
49
50/// [`Contract`] is a struct to interface with a contract. That includes things such as
51/// compiling, deploying, and running transactions against a contract.
52/// The contract has a wallet attribute, used to pay for transactions and sign them.
53/// It allows doing calls without passing a wallet/signer each time.
54pub struct Contract {
55    pub compiled_contract: CompiledContract,
56    pub wallet: WalletUnlocked,
57}
58
59impl Contract {
60    pub fn new(compiled_contract: CompiledContract, wallet: WalletUnlocked) -> Self {
61        Self {
62            compiled_contract,
63            wallet,
64        }
65    }
66
67    pub fn compute_contract_id_and_state_root(
68        compiled_contract: &CompiledContract,
69    ) -> (ContractId, Bytes32) {
70        let fuel_contract = FuelContract::from(compiled_contract.raw.clone());
71        let root = fuel_contract.root();
72        let state_root = FuelContract::initial_state_root(compiled_contract.storage_slots.iter());
73
74        let contract_id = fuel_contract.id(&compiled_contract.salt, &root, &state_root);
75
76        (contract_id, state_root)
77    }
78
79    /// Creates an ABI call based on a function [selector](Selector) and
80    /// the encoding of its call arguments, which is a slice of [`Token`]s.
81    /// It returns a prepared [`ContractCall`] that can further be used to
82    /// make the actual transaction.
83    /// This method is the underlying implementation of the functions
84    /// generated from an ABI JSON spec, i.e, this is what's generated:
85    ///
86    /// ```ignore
87    /// quote! {
88    ///     #doc
89    ///     pub fn #name(&self #input) -> #result {
90    ///         Contract::method_hash(#tokenized_signature, #arg)
91    ///     }
92    /// }
93    /// ```
94    ///
95    /// For more details see `code_gen/functions_gen.rs` in `fuels-core`.
96    ///
97    /// Note that this needs a wallet because the contract instance needs a wallet for the calls
98    pub fn method_hash<D: Tokenizable + Parameterize + Debug>(
99        provider: &Provider,
100        contract_id: Bech32ContractId,
101        wallet: &WalletUnlocked,
102        signature: Selector,
103        args: &[Token],
104        log_decoder: LogDecoder,
105    ) -> Result<ContractCallHandler<D>, Error> {
106        let encoded_selector = signature;
107
108        let tx_parameters = TxParameters::default();
109        let call_parameters = CallParameters::default();
110
111        let compute_custom_input_offset = Contract::should_compute_custom_input_offset(args);
112
113        let unresolved_bytes = ABIEncoder::encode(args)?;
114        let contract_call = ContractCall {
115            contract_id,
116            encoded_selector,
117            encoded_args: unresolved_bytes,
118            call_parameters,
119            compute_custom_input_offset,
120            variable_outputs: None,
121            message_outputs: None,
122            external_contracts: vec![],
123            output_param: D::param_type(),
124        };
125
126        Ok(ContractCallHandler {
127            contract_call,
128            tx_parameters,
129            wallet: wallet.clone(),
130            provider: provider.clone(),
131            datatype: PhantomData,
132            log_decoder,
133        })
134    }
135
136    // If the data passed into the contract method is an integer or a
137    // boolean, then the data itself should be passed. Otherwise, it
138    // should simply pass a pointer to the data in memory.
139    fn should_compute_custom_input_offset(args: &[Token]) -> bool {
140        args.len() > 1
141            || args.iter().any(|t| {
142                matches!(
143                    t,
144                    Token::String(_)
145                        | Token::Struct(_)
146                        | Token::Enum(_)
147                        | Token::B256(_)
148                        | Token::Tuple(_)
149                        | Token::Array(_)
150                        | Token::Byte(_)
151                        | Token::Vector(_)
152                )
153            })
154    }
155
156    /// Loads a compiled contract and deploys it to a running node
157    pub async fn deploy(
158        binary_filepath: &str,
159        wallet: &WalletUnlocked,
160        params: TxParameters,
161        storage_configuration: StorageConfiguration,
162    ) -> Result<Bech32ContractId, Error> {
163        let mut compiled_contract =
164            Contract::load_contract(binary_filepath, &storage_configuration.storage_path)?;
165
166        Self::merge_storage_vectors(&storage_configuration, &mut compiled_contract);
167
168        Self::deploy_loaded(&(compiled_contract), wallet, params).await
169    }
170
171    /// Loads a compiled contract with salt and deploys it to a running node
172    pub async fn deploy_with_parameters(
173        binary_filepath: &str,
174        wallet: &WalletUnlocked,
175        params: TxParameters,
176        storage_configuration: StorageConfiguration,
177        salt: Salt,
178    ) -> Result<Bech32ContractId, Error> {
179        let mut compiled_contract = Contract::load_contract_with_parameters(
180            binary_filepath,
181            &storage_configuration.storage_path,
182            salt,
183        )?;
184
185        Self::merge_storage_vectors(&storage_configuration, &mut compiled_contract);
186
187        Self::deploy_loaded(&(compiled_contract), wallet, params).await
188    }
189
190    fn merge_storage_vectors(
191        storage_configuration: &StorageConfiguration,
192        compiled_contract: &mut CompiledContract,
193    ) {
194        match &storage_configuration.manual_storage_vec {
195            Some(storage) if !storage.is_empty() => {
196                compiled_contract.storage_slots =
197                    Self::merge_storage_slots(storage, &compiled_contract.storage_slots);
198            }
199            _ => {}
200        }
201    }
202
203    /// Deploys a compiled contract to a running node
204    /// To deploy a contract, you need a wallet with enough assets to pay for deployment. This
205    /// wallet will also receive the change.
206    pub async fn deploy_loaded(
207        compiled_contract: &CompiledContract,
208        wallet: &WalletUnlocked,
209        params: TxParameters,
210    ) -> Result<Bech32ContractId, Error> {
211        let (mut tx, contract_id) =
212            Self::contract_deployment_transaction(compiled_contract, params).await?;
213
214        // The first witness is the bytecode we're deploying.
215        // The signature will be appended at position 1 of
216        // the witness list
217        wallet.add_fee_coins(&mut tx, 0, 1).await?;
218        wallet.sign_transaction(&mut tx).await?;
219
220        let provider = wallet.get_provider()?;
221        let chain_info = provider.chain_info().await?;
222
223        tx.check_without_signatures(
224            chain_info.latest_block.header.height,
225            &chain_info.consensus_parameters,
226        )?;
227        provider.send_transaction(&tx).await?;
228
229        Ok(contract_id)
230    }
231
232    pub fn load_contract(
233        binary_filepath: &str,
234        storage_path: &Option<String>,
235    ) -> Result<CompiledContract, Error> {
236        Self::load_contract_with_parameters(binary_filepath, storage_path, Salt::from([0u8; 32]))
237    }
238
239    pub fn load_contract_with_parameters(
240        binary_filepath: &str,
241        storage_path: &Option<String>,
242        salt: Salt,
243    ) -> Result<CompiledContract, Error> {
244        let extension = Path::new(binary_filepath).extension().unwrap();
245        if extension != "bin" {
246            return Err(Error::InvalidData(extension.to_str().unwrap().to_owned()));
247        }
248        let bin = std::fs::read(binary_filepath)?;
249
250        let storage = match storage_path {
251            Some(path) if Path::new(&path).exists() => Self::get_storage_vec(path),
252            Some(path) if !Path::new(&path).exists() => {
253                return Err(Error::InvalidData(path.to_owned()));
254            }
255            _ => {
256                vec![]
257            }
258        };
259
260        Ok(CompiledContract {
261            raw: bin,
262            salt,
263            storage_slots: storage,
264        })
265    }
266
267    fn merge_storage_slots(
268        manual_storage: &[StorageSlot],
269        contract_storage: &[StorageSlot],
270    ) -> Vec<StorageSlot> {
271        let mut return_storage: Vec<StorageSlot> = manual_storage.to_owned();
272        let keys: HashSet<Bytes32> = manual_storage.iter().map(|slot| *slot.key()).collect();
273
274        contract_storage.iter().for_each(|slot| {
275            if !keys.contains(slot.key()) {
276                return_storage.push(slot.clone())
277            }
278        });
279
280        return_storage
281    }
282
283    /// Crafts a transaction used to deploy a contract
284    pub async fn contract_deployment_transaction(
285        compiled_contract: &CompiledContract,
286        params: TxParameters,
287    ) -> Result<(Create, Bech32ContractId), Error> {
288        let bytecode_witness_index = 0;
289        let storage_slots: Vec<StorageSlot> = compiled_contract.storage_slots.clone();
290        let witnesses = vec![compiled_contract.raw.clone().into()];
291
292        let (contract_id, state_root) = Self::compute_contract_id_and_state_root(compiled_contract);
293
294        let outputs = vec![Output::contract_created(contract_id, state_root)];
295
296        let tx = Transaction::create(
297            params.gas_price,
298            params.gas_limit,
299            params.maturity,
300            bytecode_witness_index,
301            compiled_contract.salt,
302            storage_slots,
303            vec![],
304            outputs,
305            witnesses,
306        );
307
308        Ok((tx, contract_id.into()))
309    }
310
311    fn get_storage_vec(storage_path: &str) -> Vec<StorageSlot> {
312        let mut return_storage: Vec<StorageSlot> = vec![];
313
314        let storage_json_string = fs::read_to_string(storage_path).expect("Unable to read file");
315
316        let storage: serde_json::Value = serde_json::from_str(storage_json_string.as_str())
317            .expect("JSON was not well-formatted");
318
319        for slot in storage.as_array().unwrap() {
320            return_storage.push(StorageSlot::new(
321                Bytes32::from_str(slot["key"].as_str().unwrap()).unwrap(),
322                Bytes32::from_str(slot["value"].as_str().unwrap()).unwrap(),
323            ));
324        }
325
326        return_storage
327    }
328}
329
330#[derive(Debug)]
331/// Contains all data relevant to a single contract call
332pub struct ContractCall {
333    pub contract_id: Bech32ContractId,
334    pub encoded_args: UnresolvedBytes,
335    pub encoded_selector: Selector,
336    pub call_parameters: CallParameters,
337    pub compute_custom_input_offset: bool,
338    pub variable_outputs: Option<Vec<Output>>,
339    pub message_outputs: Option<Vec<Output>>,
340    pub external_contracts: Vec<Bech32ContractId>,
341    pub output_param: ParamType,
342}
343
344impl ContractCall {
345    pub fn with_contract_id(self, contract_id: Bech32ContractId) -> Self {
346        ContractCall {
347            contract_id,
348            ..self
349        }
350    }
351
352    pub fn with_external_contracts(
353        self,
354        external_contracts: Vec<Bech32ContractId>,
355    ) -> ContractCall {
356        ContractCall {
357            external_contracts,
358            ..self
359        }
360    }
361
362    pub fn with_variable_outputs(self, variable_outputs: Vec<Output>) -> ContractCall {
363        ContractCall {
364            variable_outputs: Some(variable_outputs),
365            ..self
366        }
367    }
368
369    pub fn with_message_outputs(self, message_outputs: Vec<Output>) -> ContractCall {
370        ContractCall {
371            message_outputs: Some(message_outputs),
372            ..self
373        }
374    }
375
376    pub fn with_call_parameters(self, call_parameters: CallParameters) -> ContractCall {
377        ContractCall {
378            call_parameters,
379            ..self
380        }
381    }
382
383    pub fn append_variable_outputs(&mut self, num: u64) {
384        let new_variable_outputs = vec![
385            Output::Variable {
386                amount: 0,
387                to: Address::zeroed(),
388                asset_id: AssetId::default(),
389            };
390            num as usize
391        ];
392
393        match self.variable_outputs {
394            Some(ref mut outputs) => outputs.extend(new_variable_outputs),
395            None => self.variable_outputs = Some(new_variable_outputs),
396        }
397    }
398
399    pub fn append_external_contracts(&mut self, contract_id: Bech32ContractId) {
400        self.external_contracts.push(contract_id)
401    }
402
403    pub fn append_message_outputs(&mut self, num: u64) {
404        let new_message_outputs = vec![
405            Output::Message {
406                recipient: Address::zeroed(),
407                amount: 0,
408            };
409            num as usize
410        ];
411
412        match self.message_outputs {
413            Some(ref mut outputs) => outputs.extend(new_message_outputs),
414            None => self.message_outputs = Some(new_message_outputs),
415        }
416    }
417
418    fn is_missing_output_variables(receipts: &[Receipt]) -> bool {
419        receipts.iter().any(
420            |r| matches!(r, Receipt::Revert { ra, .. } if *ra == FAILED_TRANSFER_TO_ADDRESS_SIGNAL),
421        )
422    }
423
424    fn find_contract_not_in_inputs(receipts: &[Receipt]) -> Option<&Receipt> {
425        receipts.iter().find(
426            |r| matches!(r, Receipt::Panic { reason, .. } if *reason.reason() == PanicReason::ContractNotInInputs ),
427        )
428    }
429}
430
431/// Based on the receipts returned by the call, the contract ID (which is null in the case of a
432/// script), and the output param, decode the values and return them.
433pub fn get_decoded_output(
434    receipts: &mut Vec<Receipt>,
435    contract_id: Option<&Bech32ContractId>,
436    output_param: &ParamType,
437) -> Result<Token, Error> {
438    // Multiple returns are handled as one `Tuple` (which has its own `ParamType`)
439    let contract_id: ContractId = match contract_id {
440        Some(contract_id) => contract_id.into(),
441        // During a script execution, the script's contract id is the **null** contract id
442        None => ContractId::new([0u8; 32]),
443    };
444    let (encoded_value, index) = match output_param.get_return_location() {
445        ReturnLocation::ReturnData => {
446            match receipts.iter().position(|receipt| {
447                matches!(receipt,
448                    Receipt::ReturnData { id, data, .. } if *id == contract_id && !data.is_empty())
449            }) {
450                Some(idx) => (
451                    receipts[idx]
452                        .data()
453                        .expect("ReturnData should have data")
454                        .to_vec(),
455                    Some(idx),
456                ),
457                None => (vec![], None),
458            }
459        }
460        ReturnLocation::Return => {
461            match receipts.iter().position(|receipt| {
462                matches!(receipt,
463                    Receipt::Return { id, ..} if *id == contract_id)
464            }) {
465                Some(idx) => (
466                    receipts[idx]
467                        .val()
468                        .expect("Return should have val")
469                        .to_be_bytes()
470                        .to_vec(),
471                    Some(idx),
472                ),
473                None => (vec![], None),
474            }
475        }
476    };
477    if let Some(i) = index {
478        receipts.remove(i);
479    }
480
481    let decoded_value = ABIDecoder::decode_single(output_param, &encoded_value)?;
482    Ok(decoded_value)
483}
484
485#[derive(Debug)]
486#[must_use = "contract calls do nothing unless you `call` them"]
487/// Helper that handles submitting a call to a client and formatting the response
488pub struct ContractCallHandler<D> {
489    pub contract_call: ContractCall,
490    pub tx_parameters: TxParameters,
491    pub wallet: WalletUnlocked,
492    pub provider: Provider,
493    pub datatype: PhantomData<D>,
494    pub log_decoder: LogDecoder,
495}
496
497impl<D> ContractCallHandler<D>
498where
499    D: Tokenizable + Debug,
500{
501    /// Sets external contracts as dependencies to this contract's call.
502    /// Effectively, this will be used to create [`Input::Contract`]/[`Output::Contract`]
503    /// pairs and set them into the transaction.
504    /// Note that this is a builder method, i.e. use it as a chain:
505    ///
506    /// ```ignore
507    /// my_contract_instance.my_method(...).set_contracts(&[another_contract_id]).call()
508    /// ```
509    ///
510    /// [`Input::Contract`]: fuel_tx::Input::Contract
511    /// [`Output::Contract`]: fuel_tx::Output::Contract
512    pub fn set_contracts(mut self, contract_ids: &[Bech32ContractId]) -> Self {
513        self.contract_call.external_contracts = contract_ids.to_vec();
514        self
515    }
516
517    /// Appends additional external contracts as dependencies to this contract's call.
518    /// Effectively, this will be used to create additional
519    /// [`Input::Contract`]/[`Output::Contract`] pairs
520    /// and set them into the transaction.
521    /// Note that this is a builder method, i.e. use it as a chain:
522    ///
523    /// ```ignore
524    /// my_contract_instance.my_method(...).append_contracts(additional_contract_id).call()
525    /// ```
526    ///
527    /// [`Input::Contract`]: fuel_tx::Input::Contract
528    /// [`Output::Contract`]: fuel_tx::Output::Contract
529    pub fn append_contract(mut self, contract_id: Bech32ContractId) -> Self {
530        self.contract_call.append_external_contracts(contract_id);
531        self
532    }
533
534    /// Sets the transaction parameters for a given transaction.
535    /// Note that this is a builder method, i.e. use it as a chain:
536
537    /// ```ignore
538    /// let params = TxParameters { gas_price: 100, gas_limit: 1000000 };
539    /// my_contract_instance.my_method(...).tx_params(params).call()
540    /// ```
541    pub fn tx_params(mut self, params: TxParameters) -> Self {
542        self.tx_parameters = params;
543        self
544    }
545
546    /// Sets the call parameters for a given contract call.
547    /// Note that this is a builder method, i.e. use it as a chain:
548    ///
549    /// ```ignore
550    /// let params = CallParameters { amount: 1, asset_id: BASE_ASSET_ID };
551    /// my_contract_instance.my_method(...).call_params(params).call()
552    /// ```
553    pub fn call_params(mut self, params: CallParameters) -> Self {
554        self.contract_call.call_parameters = params;
555        self
556    }
557
558    /// Appends `num` [`Output::Variable`]s to the transaction.
559    /// Note that this is a builder method, i.e. use it as a chain:
560    ///
561    /// ```ignore
562    /// my_contract_instance.my_method(...).add_variable_outputs(num).call()
563    /// ```
564    ///
565    /// [`Output::Variable`]: fuel_tx::Output::Variable
566    pub fn append_variable_outputs(mut self, num: u64) -> Self {
567        self.contract_call.append_variable_outputs(num);
568        self
569    }
570
571    /// Appends `num` [`Output::Message`]s to the transaction.
572    /// Note that this is a builder method, i.e. use it as a chain:
573    ///
574    /// ```ignore
575    /// my_contract_instance.my_method(...).add_message_outputs(num).call()
576    /// ```
577    ///
578    /// [`Output::Message`]: fuel_tx::Output::Message
579    pub fn append_message_outputs(mut self, num: u64) -> Self {
580        self.contract_call.append_message_outputs(num);
581        self
582    }
583
584    /// Call a contract's method on the node. If `simulate == true`, then the call is done in a
585    /// read-only manner, using a `dry-run`. The [`FuelCallResponse`] struct contains the method's
586    /// value in its `value` field as an actual typed value `D` (if your method returns `bool`,
587    /// it will be a bool, works also for structs thanks to the `abigen!()`).
588    /// The other field of [`FuelCallResponse`], `receipts`, contains the receipts of the transaction.
589    async fn call_or_simulate(&self, simulate: bool) -> Result<FuelCallResponse<D>, Error> {
590        let script = self.get_executable_call().await?;
591
592        let receipts = if simulate {
593            script.simulate(&self.provider).await?
594        } else {
595            script.execute(&self.provider).await?
596        };
597
598        self.get_response(receipts)
599    }
600
601    /// Returns the script that executes the contract call
602    pub async fn get_executable_call(&self) -> Result<ExecutableFuelCall, Error> {
603        ExecutableFuelCall::from_contract_calls(
604            std::slice::from_ref(&self.contract_call),
605            &self.tx_parameters,
606            &self.wallet,
607        )
608        .await
609    }
610
611    /// Call a contract's method on the node, in a state-modifying manner.
612    pub async fn call(self) -> Result<FuelCallResponse<D>, Error> {
613        Self::call_or_simulate(&self, false)
614            .await
615            .map_err(|err| decode_revert_error(err, &self.log_decoder))
616    }
617
618    /// Call a contract's method on the node, in a simulated manner, meaning the state of the
619    /// blockchain is *not* modified but simulated.
620    /// It is the same as the [`call`] method because the API is more user-friendly this way.
621    ///
622    /// [`call`]: Self::call
623    pub async fn simulate(self) -> Result<FuelCallResponse<D>, Error> {
624        Self::call_or_simulate(&self, true)
625            .await
626            .map_err(|err| decode_revert_error(err, &self.log_decoder))
627    }
628
629    /// Simulates a call without needing to resolve the generic for the return type
630    async fn simulate_without_decode(&self) -> Result<(), Error> {
631        let script = self.get_executable_call().await?;
632        let provider = self.wallet.get_provider()?;
633
634        script.simulate(provider).await?;
635
636        Ok(())
637    }
638
639    /// Simulates the call and attempts to resolve missing tx dependencies.
640    /// Forwards the received error if it cannot be fixed.
641    pub async fn estimate_tx_dependencies(
642        mut self,
643        max_attempts: Option<u64>,
644    ) -> Result<Self, Error> {
645        let attempts = max_attempts.unwrap_or(DEFAULT_TX_DEP_ESTIMATION_ATTEMPTS);
646
647        for _ in 0..attempts {
648            let result = self.simulate_without_decode().await;
649
650            match result {
651                Err(Error::RevertTransactionError(_, receipts))
652                    if ContractCall::is_missing_output_variables(&receipts) =>
653                {
654                    self = self.append_variable_outputs(1);
655                }
656
657                Err(Error::RevertTransactionError(_, ref receipts)) => {
658                    if let Some(receipt) = ContractCall::find_contract_not_in_inputs(receipts) {
659                        let contract_id = Bech32ContractId::from(*receipt.contract_id().unwrap());
660                        self = self.append_contract(contract_id);
661                    } else {
662                        return Err(result.expect_err("Couldn't estimate tx dependencies because we couldn't find the missing contract input"));
663                    }
664                }
665
666                Err(e) => return Err(e),
667                _ => return Ok(self),
668            }
669        }
670
671        // confirm if successful or propagate error
672        match self.call_or_simulate(true).await {
673            Ok(_) => Ok(self),
674            Err(e) => Err(e),
675        }
676    }
677
678    /// Get a contract's estimated cost
679    pub async fn estimate_transaction_cost(
680        &self,
681        tolerance: Option<f64>,
682    ) -> Result<TransactionCost, Error> {
683        let script = self.get_executable_call().await?;
684
685        let transaction_cost = self
686            .provider
687            .estimate_transaction_cost(&script.tx, tolerance)
688            .await?;
689
690        Ok(transaction_cost)
691    }
692
693    /// Create a [`FuelCallResponse`] from call receipts
694    pub fn get_response(&self, mut receipts: Vec<Receipt>) -> Result<FuelCallResponse<D>, Error> {
695        let token = get_decoded_output(
696            &mut receipts,
697            Some(&self.contract_call.contract_id),
698            &self.contract_call.output_param,
699        )?;
700        Ok(FuelCallResponse::new(
701            D::from_token(token)?,
702            receipts,
703            self.log_decoder.clone(),
704        ))
705    }
706}
707
708#[derive(Debug)]
709#[must_use = "contract calls do nothing unless you `call` them"]
710/// Helper that handles bundling multiple calls into a single transaction
711pub struct MultiContractCallHandler {
712    pub contract_calls: Vec<ContractCall>,
713    pub log_decoder: LogDecoder,
714    pub tx_parameters: TxParameters,
715    pub wallet: WalletUnlocked,
716}
717
718impl MultiContractCallHandler {
719    pub fn new(wallet: WalletUnlocked) -> Self {
720        Self {
721            contract_calls: vec![],
722            tx_parameters: TxParameters::default(),
723            wallet,
724            log_decoder: LogDecoder {
725                logs_map: HashMap::new(),
726            },
727        }
728    }
729
730    /// Adds a contract call to be bundled in the transaction
731    /// Note that this is a builder method
732    pub fn add_call<D: Tokenizable>(&mut self, call_handler: ContractCallHandler<D>) -> &mut Self {
733        self.log_decoder.merge(&call_handler.log_decoder);
734        self.contract_calls.push(call_handler.contract_call);
735        self
736    }
737
738    /// Sets the transaction parameters for a given transaction.
739    /// Note that this is a builder method
740    pub fn tx_params(&mut self, params: TxParameters) -> &mut Self {
741        self.tx_parameters = params;
742        self
743    }
744
745    /// Returns the script that executes the contract calls
746    pub async fn get_executable_call(&self) -> Result<ExecutableFuelCall, Error> {
747        if self.contract_calls.is_empty() {
748            panic!("No calls added. Have you used '.add_calls()'?");
749        }
750
751        ExecutableFuelCall::from_contract_calls(
752            &self.contract_calls,
753            &self.tx_parameters,
754            &self.wallet,
755        )
756        .await
757    }
758
759    /// Call contract methods on the node, in a state-modifying manner.
760    pub async fn call<D: Tokenizable + Debug>(&self) -> Result<FuelCallResponse<D>, Error> {
761        Self::call_or_simulate(self, false)
762            .await
763            .map_err(|err| decode_revert_error(err, &self.log_decoder))
764    }
765
766    /// Call contract methods on the node, in a simulated manner, meaning the state of the
767    /// blockchain is *not* modified but simulated.
768    /// It is the same as the [`call`] method because the API is more user-friendly this way.
769    ///
770    /// [`call`]: Self::call
771    pub async fn simulate<D: Tokenizable + Debug>(&self) -> Result<FuelCallResponse<D>, Error> {
772        Self::call_or_simulate(self, true)
773            .await
774            .map_err(|err| decode_revert_error(err, &self.log_decoder))
775    }
776
777    async fn call_or_simulate<D: Tokenizable + Debug>(
778        &self,
779        simulate: bool,
780    ) -> Result<FuelCallResponse<D>, Error> {
781        let script = self.get_executable_call().await?;
782
783        let provider = self.wallet.get_provider()?;
784
785        let receipts = if simulate {
786            script.simulate(provider).await?
787        } else {
788            script.execute(provider).await?
789        };
790
791        self.get_response(receipts)
792    }
793
794    /// Simulates a call without needing to resolve the generic for the return type
795    async fn simulate_without_decode(&self) -> Result<(), Error> {
796        let script = self.get_executable_call().await?;
797        let provider = self.wallet.get_provider()?;
798
799        script.simulate(provider).await?;
800
801        Ok(())
802    }
803
804    /// Simulates the call and attempts to resolve missing tx dependencies.
805    /// Forwards the received error if it cannot be fixed.
806    pub async fn estimate_tx_dependencies(
807        mut self,
808        max_attempts: Option<u64>,
809    ) -> Result<Self, Error> {
810        let attempts = max_attempts.unwrap_or(DEFAULT_TX_DEP_ESTIMATION_ATTEMPTS);
811
812        for _ in 0..attempts {
813            let result = self.simulate_without_decode().await;
814
815            match result {
816                Err(Error::RevertTransactionError(_, receipts))
817                    if ContractCall::is_missing_output_variables(&receipts) =>
818                {
819                    self.contract_calls
820                        .iter_mut()
821                        .take(1)
822                        .for_each(|call| call.append_variable_outputs(1));
823                }
824
825                Err(Error::RevertTransactionError(_, ref receipts)) => {
826                    if let Some(receipt) = ContractCall::find_contract_not_in_inputs(receipts) {
827                        let contract_id = Bech32ContractId::from(*receipt.contract_id().unwrap());
828                        self.contract_calls
829                            .iter_mut()
830                            .take(1)
831                            .for_each(|call| call.append_external_contracts(contract_id.clone()));
832                    } else {
833                        return Err(result.expect_err("Couldn't estimate tx dependencies because we couldn't find the missing contract input"));
834                    }
835                }
836
837                Err(e) => return Err(e),
838                _ => return Ok(self),
839            }
840        }
841
842        Ok(self)
843    }
844
845    /// Get a contract's estimated cost
846    pub async fn estimate_transaction_cost(
847        &self,
848        tolerance: Option<f64>,
849    ) -> Result<TransactionCost, Error> {
850        let script = self.get_executable_call().await?;
851
852        let transaction_cost = self
853            .wallet
854            .get_provider()?
855            .estimate_transaction_cost(&script.tx, tolerance)
856            .await?;
857
858        Ok(transaction_cost)
859    }
860
861    /// Create a [`FuelCallResponse`] from call receipts
862    pub fn get_response<D: Tokenizable + Debug>(
863        &self,
864        mut receipts: Vec<Receipt>,
865    ) -> Result<FuelCallResponse<D>, Error> {
866        let mut final_tokens = vec![];
867
868        for call in self.contract_calls.iter() {
869            let decoded =
870                get_decoded_output(&mut receipts, Some(&call.contract_id), &call.output_param)?;
871
872            final_tokens.push(decoded.clone());
873        }
874
875        let tokens_as_tuple = Token::Tuple(final_tokens);
876        let response = FuelCallResponse::<D>::new(
877            D::from_token(tokens_as_tuple)?,
878            receipts,
879            self.log_decoder.clone(),
880        );
881
882        Ok(response)
883    }
884}
885
886#[cfg(test)]
887mod test {
888    use fuels_test_helpers::launch_provider_and_get_wallet;
889
890    use super::*;
891
892    #[tokio::test]
893    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidData(\"json\")")]
894    async fn deploy_panics_on_non_binary_file() {
895        let wallet = launch_provider_and_get_wallet().await;
896
897        // Should panic as we are passing in a JSON instead of BIN
898        Contract::deploy(
899            "tests/types/contract_output_test/out/debug/contract_output_test-abi.json",
900            &wallet,
901            TxParameters::default(),
902            StorageConfiguration::default(),
903        )
904        .await
905        .unwrap();
906    }
907
908    #[tokio::test]
909    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidData(\"json\")")]
910    async fn deploy_with_salt_panics_on_non_binary_file() {
911        let wallet = launch_provider_and_get_wallet().await;
912
913        // Should panic as we are passing in a JSON instead of BIN
914        Contract::deploy_with_parameters(
915            "tests/types/contract_output_test/out/debug/contract_output_test-abi.json",
916            &wallet,
917            TxParameters::default(),
918            StorageConfiguration::default(),
919            Salt::default(),
920        )
921        .await
922        .unwrap();
923    }
924}