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
33pub 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"]
42pub 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 cached_tx_id: Option<Bytes32>,
52 variable_output_policy: VariableOutputPolicy,
53}
54
55impl<A, C, T> CallHandler<A, C, T> {
56 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}