fuels_programs/calls/
call_handler.rs

1use core::{fmt::Debug, marker::PhantomData};
2
3use fuel_tx::{AssetId, Bytes32, Receipt};
4use fuels_accounts::{provider::TransactionCost, Account};
5use fuels_core::{
6    codec::{ABIEncoder, DecoderConfig, EncoderConfig, LogDecoder},
7    traits::{Parameterize, Tokenizable},
8    types::{
9        bech32::{Bech32Address, Bech32ContractId},
10        errors::{error, transaction::Reason, Error, Result},
11        input::Input,
12        output::Output,
13        transaction::{ScriptTransaction, Transaction, TxPolicies},
14        transaction_builders::{
15            BuildableTransaction, ScriptBuildStrategy, ScriptTransactionBuilder,
16            VariableOutputPolicy,
17        },
18        tx_status::TxStatus,
19        Selector, Token,
20    },
21};
22
23use crate::{
24    calls::{
25        receipt_parser::ReceiptParser,
26        traits::{ContractDependencyConfigurator, ResponseParser, TransactionTuner},
27        utils::find_id_of_missing_contract,
28        CallParameters, ContractCall, Execution, ScriptCall,
29    },
30    responses::{CallResponse, SubmitResponse},
31};
32
33// Trait implemented by contract instances so that
34// they can be passed to the `with_contracts` method
35pub trait ContractDependency {
36    fn id(&self) -> Bech32ContractId;
37    fn log_decoder(&self) -> LogDecoder;
38}
39
40#[derive(Debug, Clone)]
41#[must_use = "contract calls do nothing unless you `call` them"]
42/// Helper that handles submitting a call to a client and formatting the response
43pub struct CallHandler<A, C, T> {
44    pub account: A,
45    pub call: C,
46    pub tx_policies: TxPolicies,
47    pub log_decoder: LogDecoder,
48    pub datatype: PhantomData<T>,
49    decoder_config: DecoderConfig,
50    // Initially `None`, gets set to the right tx id after the transaction is submitted
51    cached_tx_id: Option<Bytes32>,
52    variable_output_policy: VariableOutputPolicy,
53}
54
55impl<A, C, T> CallHandler<A, C, T> {
56    /// Sets the transaction policies for a given transaction.
57    /// Note that this is a builder method, i.e. use it as a chain:
58    /// ```ignore
59    /// let tx_policies = TxPolicies::default().with_gas_price(100);
60    /// my_contract_instance.my_method(...).with_tx_policies(tx_policies).call()
61    /// ```
62    pub fn with_tx_policies(mut self, tx_policies: TxPolicies) -> Self {
63        self.tx_policies = tx_policies;
64        self
65    }
66
67    pub fn with_decoder_config(mut self, decoder_config: DecoderConfig) -> Self {
68        self.decoder_config = decoder_config;
69        self.log_decoder.set_decoder_config(decoder_config);
70        self
71    }
72
73    /// If this method is not called, the default policy is to not add any variable outputs.
74    ///
75    /// # Parameters
76    /// - `variable_outputs`: The [`VariableOutputPolicy`] to apply for the contract call.
77    ///
78    /// # Returns
79    /// - `Self`: The updated SDK configuration.
80    pub fn with_variable_output_policy(mut self, variable_outputs: VariableOutputPolicy) -> Self {
81        self.variable_output_policy = variable_outputs;
82        self
83    }
84}
85
86impl<A, C, T> CallHandler<A, C, T>
87where
88    A: Account,
89    C: TransactionTuner,
90    T: Tokenizable + Parameterize + Debug,
91{
92    pub async fn transaction_builder(&self) -> Result<ScriptTransactionBuilder> {
93        self.call
94            .transaction_builder(self.tx_policies, self.variable_output_policy, &self.account)
95            .await
96    }
97
98    /// Returns the script that executes the contract call
99    pub async fn build_tx(&self) -> Result<ScriptTransaction> {
100        self.call
101            .build_tx(self.tx_policies, self.variable_output_policy, &self.account)
102            .await
103    }
104
105    /// Get a call's estimated cost
106    pub async fn estimate_transaction_cost(
107        &self,
108        tolerance: Option<f64>,
109        block_horizon: Option<u32>,
110    ) -> Result<TransactionCost> {
111        let tx = self.build_tx().await?;
112        let provider = self.account.try_provider()?;
113
114        let transaction_cost = provider
115            .estimate_transaction_cost(tx, tolerance, block_horizon)
116            .await?;
117
118        Ok(transaction_cost)
119    }
120}
121
122impl<A, C, T> CallHandler<A, C, T>
123where
124    A: Account,
125    C: ContractDependencyConfigurator + TransactionTuner + ResponseParser,
126    T: Tokenizable + Parameterize + Debug,
127{
128    /// Sets external contracts as dependencies to this contract's call.
129    /// Effectively, this will be used to create [`fuel_tx::Input::Contract`]/[`fuel_tx::Output::Contract`]
130    /// pairs and set them into the transaction. Note that this is a builder
131    /// method, i.e. use it as a chain:
132    ///
133    /// ```ignore
134    /// my_contract_instance.my_method(...).with_contract_ids(&[another_contract_id]).call()
135    /// ```
136    ///
137    /// [`Input::Contract`]: fuel_tx::Input::Contract
138    /// [`Output::Contract`]: fuel_tx::Output::Contract
139    pub fn with_contract_ids(mut self, contract_ids: &[Bech32ContractId]) -> Self {
140        self.call = self.call.with_external_contracts(contract_ids.to_vec());
141
142        self
143    }
144
145    /// Sets external contract instances as dependencies to this contract's call.
146    /// Effectively, this will be used to: merge `LogDecoder`s and create
147    /// [`fuel_tx::Input::Contract`]/[`fuel_tx::Output::Contract`] pairs and set them into the transaction.
148    /// Note that this is a builder method, i.e. use it as a chain:
149    ///
150    /// ```ignore
151    /// my_contract_instance.my_method(...).with_contracts(&[another_contract_instance]).call()
152    /// ```
153    pub fn with_contracts(mut self, contracts: &[&dyn ContractDependency]) -> Self {
154        self.call = self
155            .call
156            .with_external_contracts(contracts.iter().map(|c| c.id()).collect());
157        for c in contracts {
158            self.log_decoder.merge(c.log_decoder());
159        }
160
161        self
162    }
163
164    /// Call a contract's method on the node, in a state-modifying manner.
165    pub async fn call(mut self) -> Result<CallResponse<T>> {
166        let tx = self.build_tx().await?;
167        let provider = self.account.try_provider()?;
168
169        let consensus_parameters = provider.consensus_parameters().await?;
170        let chain_id = consensus_parameters.chain_id();
171        self.cached_tx_id = Some(tx.id(chain_id));
172
173        let tx_status = provider.send_transaction_and_await_commit(tx).await?;
174
175        let receipts = tx_status.take_receipts_checked(Some(&self.log_decoder))?;
176
177        self.get_response(receipts)
178    }
179
180    pub async fn submit(mut self) -> Result<SubmitResponse<A, C, T>> {
181        let tx = self.build_tx().await?;
182        let provider = self.account.try_provider()?;
183
184        let tx_id = provider.send_transaction(tx.clone()).await?;
185        self.cached_tx_id = Some(tx_id);
186
187        Ok(SubmitResponse::<A, C, T>::new(tx_id, self))
188    }
189
190    /// Call a contract's method on the node, in a simulated manner, meaning the state of the
191    /// blockchain is *not* modified but simulated.
192    pub async fn simulate(&mut self, execution: Execution) -> Result<CallResponse<T>> {
193        let provider = self.account.try_provider()?;
194
195        let tx_status = if let Execution::StateReadOnly = execution {
196            let tx = self
197                .transaction_builder()
198                .await?
199                .with_build_strategy(ScriptBuildStrategy::StateReadOnly)
200                .build(provider)
201                .await?;
202
203            provider.dry_run_opt(tx, false, Some(0)).await?
204        } else {
205            let tx = self.build_tx().await?;
206            provider.dry_run(tx).await?
207        };
208        let receipts = tx_status.take_receipts_checked(Some(&self.log_decoder))?;
209
210        self.get_response(receipts)
211    }
212
213    /// Create a [`CallResponse`] from call receipts
214    pub fn get_response(&self, receipts: Vec<Receipt>) -> Result<CallResponse<T>> {
215        let token = self
216            .call
217            .parse_call(&receipts, self.decoder_config, &T::param_type())?;
218
219        Ok(CallResponse::new(
220            T::from_token(token)?,
221            receipts,
222            self.log_decoder.clone(),
223            self.cached_tx_id,
224        ))
225    }
226
227    /// Create a [`CallResponse`] from `TxStatus`
228    pub fn get_response_from(&self, tx_status: TxStatus) -> Result<CallResponse<T>> {
229        let receipts = tx_status.take_receipts_checked(Some(&self.log_decoder))?;
230
231        self.get_response(receipts)
232    }
233
234    pub async fn determine_missing_contracts(mut self, max_attempts: Option<u64>) -> Result<Self> {
235        let attempts = max_attempts.unwrap_or(10);
236
237        for _ in 0..attempts {
238            match self.simulate(Execution::Realistic).await {
239                Ok(_) => return Ok(self),
240
241                Err(Error::Transaction(Reason::Reverted { ref receipts, .. })) => {
242                    if let Some(contract_id) = find_id_of_missing_contract(receipts) {
243                        self.call.append_external_contract(contract_id);
244                    }
245                }
246
247                Err(other_error) => return Err(other_error),
248            }
249        }
250
251        self.simulate(Execution::Realistic).await.map(|_| self)
252    }
253}
254
255impl<A, T> CallHandler<A, ContractCall, T>
256where
257    A: Account,
258    T: Tokenizable + Parameterize + Debug,
259{
260    pub fn new_contract_call(
261        contract_id: Bech32ContractId,
262        account: A,
263        encoded_selector: Selector,
264        args: &[Token],
265        log_decoder: LogDecoder,
266        is_payable: bool,
267        encoder_config: EncoderConfig,
268    ) -> Self {
269        let call = ContractCall {
270            contract_id,
271            encoded_selector,
272            encoded_args: ABIEncoder::new(encoder_config).encode(args),
273            call_parameters: CallParameters::default(),
274            external_contracts: vec![],
275            output_param: T::param_type(),
276            is_payable,
277            custom_assets: Default::default(),
278        };
279        CallHandler {
280            account,
281            call,
282            tx_policies: TxPolicies::default(),
283            log_decoder,
284            datatype: PhantomData,
285            decoder_config: DecoderConfig::default(),
286            cached_tx_id: None,
287            variable_output_policy: VariableOutputPolicy::default(),
288        }
289    }
290
291    /// Adds a custom `asset_id` with its `amount` and an optional `address` to be used for
292    /// generating outputs to this contract's call.
293    ///
294    /// # Parameters
295    /// - `asset_id`: The unique identifier of the asset being added.
296    /// - `amount`: The amount of the asset being added.
297    /// - `address`: The optional account address that the output amount will be sent to.
298    ///              If not provided, the asset will be sent to the users account address.
299    ///
300    /// Note that this is a builder method, i.e. use it as a chain:
301    ///
302    /// ```ignore
303    /// let asset_id = AssetId::from([3u8; 32]);
304    /// let amount = 5000;
305    /// my_contract_instance.my_method(...).add_custom_asset(asset_id, amount, None).call()
306    /// ```
307    pub fn add_custom_asset(
308        mut self,
309        asset_id: AssetId,
310        amount: u64,
311        to: Option<Bech32Address>,
312    ) -> Self {
313        self.call.add_custom_asset(asset_id, amount, to);
314        self
315    }
316
317    pub fn is_payable(&self) -> bool {
318        self.call.is_payable
319    }
320
321    /// Sets the call parameters for a given contract call.
322    /// Note that this is a builder method, i.e. use it as a chain:
323    ///
324    /// ```ignore
325    /// let params = CallParameters { amount: 1, asset_id: AssetId::zeroed() };
326    /// my_contract_instance.my_method(...).call_params(params).call()
327    /// ```
328    pub fn call_params(mut self, params: CallParameters) -> Result<Self> {
329        if !self.is_payable() && params.amount() > 0 {
330            return Err(error!(Other, "assets forwarded to non-payable method"));
331        }
332        self.call.call_parameters = params;
333
334        Ok(self)
335    }
336}
337
338impl<A, T> CallHandler<A, ScriptCall, T>
339where
340    A: Account,
341    T: Parameterize + Tokenizable + Debug,
342{
343    pub fn new_script_call(
344        script_binary: Vec<u8>,
345        encoded_args: Result<Vec<u8>>,
346        account: A,
347        log_decoder: LogDecoder,
348    ) -> Self {
349        let call = ScriptCall {
350            script_binary,
351            encoded_args,
352            inputs: vec![],
353            outputs: vec![],
354            external_contracts: vec![],
355        };
356
357        Self {
358            account,
359            call,
360            tx_policies: TxPolicies::default(),
361            log_decoder,
362            datatype: PhantomData,
363            decoder_config: DecoderConfig::default(),
364            cached_tx_id: None,
365            variable_output_policy: VariableOutputPolicy::default(),
366        }
367    }
368
369    pub fn with_outputs(mut self, outputs: Vec<Output>) -> Self {
370        self.call = self.call.with_outputs(outputs);
371        self
372    }
373
374    pub fn with_inputs(mut self, inputs: Vec<Input>) -> Self {
375        self.call = self.call.with_inputs(inputs);
376        self
377    }
378}
379
380impl<A> CallHandler<A, Vec<ContractCall>, ()>
381where
382    A: Account,
383{
384    pub fn new_multi_call(account: A) -> Self {
385        Self {
386            account,
387            call: vec![],
388            tx_policies: TxPolicies::default(),
389            log_decoder: LogDecoder::new(Default::default()),
390            datatype: PhantomData,
391            decoder_config: DecoderConfig::default(),
392            cached_tx_id: None,
393            variable_output_policy: VariableOutputPolicy::default(),
394        }
395    }
396
397    fn append_external_contract(mut self, contract_id: Bech32ContractId) -> Result<Self> {
398        if self.call.is_empty() {
399            return Err(error!(
400                Other,
401                "no calls added. Have you used '.add_calls()'?"
402            ));
403        }
404
405        self.call
406            .iter_mut()
407            .take(1)
408            .for_each(|call| call.append_external_contract(contract_id.clone()));
409
410        Ok(self)
411    }
412
413    /// Adds a contract call to be bundled in the transaction
414    /// Note that this is a builder method
415    pub fn add_call(
416        mut self,
417        call_handler: CallHandler<impl Account, ContractCall, impl Tokenizable>,
418    ) -> Self {
419        self.log_decoder.merge(call_handler.log_decoder);
420        self.call.push(call_handler.call);
421
422        self
423    }
424
425    /// Call contract methods on the node, in a state-modifying manner.
426    pub async fn call<T: Tokenizable + Debug>(mut self) -> Result<CallResponse<T>> {
427        let tx = self.build_tx().await?;
428
429        let provider = self.account.try_provider()?;
430        let consensus_parameters = provider.consensus_parameters().await?;
431        let chain_id = consensus_parameters.chain_id();
432
433        self.cached_tx_id = Some(tx.id(chain_id));
434
435        let tx_status = provider.send_transaction_and_await_commit(tx).await?;
436
437        let receipts = tx_status.take_receipts_checked(Some(&self.log_decoder))?;
438        self.get_response(receipts)
439    }
440
441    pub async fn submit(mut self) -> Result<SubmitResponse<A, Vec<ContractCall>, ()>> {
442        let tx = self.build_tx().await?;
443        let provider = self.account.try_provider()?;
444
445        let tx_id = provider.send_transaction(tx).await?;
446        self.cached_tx_id = Some(tx_id);
447
448        Ok(SubmitResponse::<A, Vec<ContractCall>, ()>::new(tx_id, self))
449    }
450
451    /// Call contract methods on the node, in a simulated manner, meaning the state of the
452    /// blockchain is *not* modified but simulated.
453    /// It is the same as the [call] method because the API is more user-friendly this way.
454    ///
455    /// [call]: Self::call
456    pub async fn simulate<T: Tokenizable + Debug>(
457        &mut self,
458        execution: Execution,
459    ) -> Result<CallResponse<T>> {
460        let provider = self.account.try_provider()?;
461
462        let tx_status = if let Execution::StateReadOnly = execution {
463            let tx = self
464                .transaction_builder()
465                .await?
466                .with_build_strategy(ScriptBuildStrategy::StateReadOnly)
467                .build(provider)
468                .await?;
469
470            provider.dry_run_opt(tx, false, Some(0)).await?
471        } else {
472            let tx = self.build_tx().await?;
473            provider.dry_run(tx).await?
474        };
475        let receipts = tx_status.take_receipts_checked(Some(&self.log_decoder))?;
476
477        self.get_response(receipts)
478    }
479
480    /// Simulates a call without needing to resolve the generic for the return type
481    async fn simulate_without_decode(&self) -> Result<()> {
482        let provider = self.account.try_provider()?;
483        let tx = self.build_tx().await?;
484
485        provider.dry_run(tx).await?.check(None)?;
486
487        Ok(())
488    }
489
490    /// Create a [`CallResponse`] from call receipts
491    pub fn get_response<T: Tokenizable + Debug>(
492        &self,
493        receipts: Vec<Receipt>,
494    ) -> Result<CallResponse<T>> {
495        let mut receipt_parser = ReceiptParser::new(&receipts, self.decoder_config);
496
497        let final_tokens = self
498            .call
499            .iter()
500            .map(|call| receipt_parser.parse_call(&call.contract_id, &call.output_param))
501            .collect::<Result<Vec<_>>>()?;
502
503        let tokens_as_tuple = Token::Tuple(final_tokens);
504        let response = CallResponse::<T>::new(
505            T::from_token(tokens_as_tuple)?,
506            receipts,
507            self.log_decoder.clone(),
508            self.cached_tx_id,
509        );
510
511        Ok(response)
512    }
513
514    /// Simulates the call and attempts to resolve missing contract outputs.
515    /// Forwards the received error if it cannot be fixed.
516    pub async fn determine_missing_contracts(mut self, max_attempts: Option<u64>) -> Result<Self> {
517        let attempts = max_attempts.unwrap_or(10);
518
519        for _ in 0..attempts {
520            match self.simulate_without_decode().await {
521                Ok(_) => return Ok(self),
522
523                Err(Error::Transaction(Reason::Reverted { ref receipts, .. })) => {
524                    if let Some(contract_id) = find_id_of_missing_contract(receipts) {
525                        self = self.append_external_contract(contract_id)?;
526                    }
527                }
528
529                Err(other_error) => return Err(other_error),
530            }
531        }
532
533        self.simulate_without_decode().await.map(|_| self)
534    }
535}