1use crate::{
2 order_book::DEFAULT_METHOD_GAS,
3 utxo_manager::{
4 FuelTxCoin,
5 SharedUtxoManager,
6 },
7 wallet_ext::{
8 BuilderData,
9 SendResult,
10 WalletExt,
11 receipts_of,
12 },
13};
14use fuel_core_client::client::{
15 FuelClient,
16 types::TransactionStatus,
17};
18use fuel_core_types::{
19 blockchain::transaction::TransactionExt,
20 fuel_tx::{
21 Chargeable,
22 Finalizable,
23 Input,
24 Output,
25 Receipt,
26 Script,
27 Transaction,
28 TxId,
29 TxPointer,
30 UniqueIdentifier,
31 },
32 fuel_types::ContractId,
33 services::executor::TransactionExecutionResult,
34};
35use fuels::{
36 accounts::ViewOnlyAccount,
37 core::traits::{
38 Parameterize,
39 Tokenizable,
40 },
41 prelude::{
42 CallHandler,
43 Wallet,
44 },
45 programs::{
46 calls::{
47 ContractCall,
48 ScriptCall,
49 traits::TransactionTuner,
50 utils::find_ids_of_missing_contracts,
51 },
52 responses::CallResponse,
53 },
54 types::{
55 BlockHeight,
56 errors::{
57 Error as FuelsError,
58 Result as FuelsResult,
59 },
60 transaction_builders::VariableOutputPolicy,
61 tx_status::TxStatus,
62 },
63};
64use std::{
65 collections::HashSet,
66 fmt::Debug,
67 future::Future,
68};
69
70pub trait CallHandlerExt<T> {
71 fn almost_sync_call(
82 self,
83 builder_date: &BuilderData,
84 utxo_manager: &SharedUtxoManager,
85 tx_config: &Option<TransactionConfig>,
86 dry_run_client: Option<&FuelClient>,
87 submit_clients: &[FuelClient],
88 ) -> impl Future<Output = FuelsResult<SendResult<FuelsResult<CallResponse<T>>>>>;
89}
90
91trait CallResponseDecoder<T> {
94 fn decode_response(&self, tx_status: TxStatus) -> FuelsResult<CallResponse<T>>;
95}
96
97impl<T> CallResponseDecoder<T> for CallHandler<Wallet, ContractCall, T>
98where
99 T: Tokenizable + Parameterize + Debug,
100{
101 fn decode_response(&self, tx_status: TxStatus) -> FuelsResult<CallResponse<T>> {
102 self.get_response(tx_status)
103 }
104}
105
106impl<T> CallResponseDecoder<T> for CallHandler<Wallet, ScriptCall, T>
107where
108 T: Tokenizable + Parameterize + Debug,
109{
110 fn decode_response(&self, tx_status: TxStatus) -> FuelsResult<CallResponse<T>> {
111 self.get_response(tx_status)
112 }
113}
114
115impl<T> CallResponseDecoder<T> for CallHandler<Wallet, Vec<ContractCall>, ()>
116where
117 T: Tokenizable + Parameterize + Debug,
118{
119 fn decode_response(&self, tx_status: TxStatus) -> FuelsResult<CallResponse<T>> {
120 self.get_response(tx_status)
121 }
122}
123
124#[derive(Debug, Clone, Copy, Default)]
125pub struct TransactionConfig {
126 pub min_gas_limit: u64,
127 pub estimate_gas_usage: bool,
128 pub expiration_height: Option<BlockHeight>,
129 pub max_fee_coins: Option<usize>,
132}
133
134impl TransactionConfig {
135 pub fn builder() -> TransactionConfigBuilder {
136 TransactionConfigBuilder::new()
137 }
138}
139
140#[derive(Debug, Clone, Copy, Default)]
141pub struct TransactionConfigBuilder {
142 min_gas_limit: Option<u64>,
143 estimate_gas_usage: Option<bool>,
144 expiration_height: Option<BlockHeight>,
145 max_fee_coins: Option<usize>,
146}
147
148impl TransactionConfigBuilder {
149 pub fn new() -> Self {
150 TransactionConfigBuilder {
151 min_gas_limit: None,
152 estimate_gas_usage: None,
153 expiration_height: None,
154 max_fee_coins: None,
155 }
156 }
157
158 pub fn with_min_gas_limit(&mut self, min_gas_limit: u64) -> &mut Self {
159 self.min_gas_limit = Some(min_gas_limit);
160 self
161 }
162
163 pub fn with_estimate_gas_usage(&mut self, estimate_gas_usage: bool) -> &mut Self {
164 self.estimate_gas_usage = Some(estimate_gas_usage);
165 self
166 }
167
168 pub fn min_gas_limit(&self) -> Option<u64> {
169 self.min_gas_limit
170 }
171
172 pub fn estimate_gas_usage(&self) -> Option<bool> {
173 self.estimate_gas_usage
174 }
175
176 pub fn with_expiration_height(
177 &mut self,
178 expiration_height: BlockHeight,
179 ) -> &mut Self {
180 self.expiration_height = Some(expiration_height);
181 self
182 }
183
184 pub fn expiration_height(&self) -> Option<BlockHeight> {
185 self.expiration_height
186 }
187
188 pub fn with_max_fee_coins(&mut self, max_fee_coins: usize) -> &mut Self {
189 self.max_fee_coins = Some(max_fee_coins);
190 self
191 }
192
193 pub fn max_fee_coins(&self) -> Option<usize> {
194 self.max_fee_coins
195 }
196
197 pub fn build(self) -> TransactionConfig {
198 TransactionConfig {
199 min_gas_limit: self.min_gas_limit.unwrap_or(DEFAULT_METHOD_GAS),
200 estimate_gas_usage: self.estimate_gas_usage.unwrap_or(true),
201 expiration_height: self.expiration_height,
202 max_fee_coins: self.max_fee_coins,
203 }
204 }
205}
206
207impl<C, D, T> CallHandlerExt<T> for CallHandler<Wallet, C, D>
208where
209 C: TransactionTuner,
210 D: Tokenizable + Parameterize + Debug,
211 T: Tokenizable + Parameterize + Debug,
212 CallHandler<Wallet, C, D>: CallResponseDecoder<T>,
213{
214 #[tracing::instrument(skip_all)]
215 async fn almost_sync_call(
216 self,
217 builder_date: &BuilderData,
218 utxo_manager: &SharedUtxoManager,
219 tx_config: &Option<TransactionConfig>,
220 dry_run_client: Option<&FuelClient>,
221 submit_clients: &[FuelClient],
222 ) -> FuelsResult<SendResult<FuelsResult<CallResponse<T>>>> {
223 let tx_config = tx_config.unwrap_or_default();
224 let consensus_parameters = &builder_date.consensus_parameters;
225 let tb =
226 self.transaction_builder_with_parameters(consensus_parameters, vec![])?;
227
228 let owner = self.account.address();
229 let secret_key = self.account.signer().secret_key();
230 let base_asset_id = *consensus_parameters.base_asset_id();
231
232 let max_fee = builder_date.max_fee();
233
234 let max_fee_coins = tx_config.max_fee_coins.unwrap_or(usize::MAX);
235
236 let input_coins = {
237 let mut utxo_manager = utxo_manager.lock().await;
238 utxo_manager
239 .guaranteed_extract_coins(
240 owner,
241 base_asset_id,
242 max_fee as u128,
243 max_fee_coins,
244 )
245 .map_err(|e| FuelsError::Other(e.to_string()))
246 }?;
247 let coins_iter = input_coins.iter();
248 let account = self.account.clone();
249
250 let assemble_tx = async move {
251 let witness_limit = crate::wallet_ext::SIGNATURE_MARGIN;
252
253 let mut builder =
254 fuel_core_types::fuel_tx::TransactionBuilder::<Script>::script(
255 tb.script,
256 tb.script_data,
257 );
258 builder
259 .with_chain_id(consensus_parameters.chain_id())
260 .max_fee_limit(max_fee)
261 .witness_limit(witness_limit as u64);
262
263 if let Some(expiration_height) = tx_config.expiration_height {
264 builder.expiration(expiration_height);
265 }
266
267 for coin in coins_iter {
268 builder.add_unsigned_coin_input(
269 secret_key,
270 coin.utxo_id,
271 coin.amount,
272 coin.asset_id,
273 TxPointer::default(),
274 );
275 }
276
277 builder.add_output(Output::Change {
278 to: owner,
279 amount: 0,
280 asset_id: base_asset_id,
281 });
282
283 for input in tb.inputs {
284 if let fuels::types::input::Input::Contract { contract_id, .. } = input {
285 let contract_index = builder.inputs().len();
286 builder.add_input(Input::contract(
287 Default::default(),
288 Default::default(),
289 Default::default(),
290 Default::default(),
291 contract_id,
292 ));
293 builder.add_output(Output::contract(
294 contract_index as u16,
295 Default::default(),
296 Default::default(),
297 ));
298 }
299 }
300
301 if let VariableOutputPolicy::Exactly(variable_outputs) =
303 tb.variable_output_policy
304 {
305 for _ in 0..variable_outputs {
306 builder.add_output(Output::Variable {
307 to: Default::default(),
308 amount: 0,
309 asset_id: Default::default(),
310 });
311 }
312 }
313
314 let dummy_script = builder.clone().finalize();
315 let max_gas = dummy_script.max_gas(
316 consensus_parameters.gas_costs(),
317 consensus_parameters.fee_params(),
318 ) + 1;
319 let available_gas =
320 consensus_parameters.tx_params().max_gas_per_tx() - max_gas;
321
322 let (missing_contracts, used_gas) = if tx_config.estimate_gas_usage {
323 builder.script_gas_limit(available_gas);
324
325 let client =
326 dry_run_client.unwrap_or_else(|| account.provider().client());
327 let tx_to_dry_run = builder.clone().finalize().into();
328
329 let result = client
330 .dry_run_opt(
331 &[tx_to_dry_run],
332 Some(false),
333 Some(builder_date.gas_price),
334 None,
335 )
336 .await?
337 .into_iter()
338 .next()
339 .ok_or_else(|| {
340 FuelsError::Other("Dry run failed to return a result".to_string())
341 })?;
342
343 result.result.missing_contracts_and_used_gas()
344 } else {
345 (Default::default(), 0)
346 };
347
348 for contract_id in missing_contracts {
349 let contract_index = builder.inputs().len();
350 builder.add_input(Input::contract(
351 Default::default(),
352 Default::default(),
353 Default::default(),
354 Default::default(),
355 contract_id,
356 ));
357
358 builder.add_output(Output::contract(
359 contract_index as u16,
360 Default::default(),
361 Default::default(),
362 ));
363 }
364
365 let gas_limit = std::cmp::max(
366 tx_config.min_gas_limit,
367 std::cmp::min(used_gas * 2 + 100_000, available_gas),
368 );
369 builder.script_gas_limit(gas_limit);
370
371 Ok(builder.finalize_as_transaction())
372 };
373
374 let tx = match assemble_tx.await {
375 Ok(tx) => tx,
376 Err(e) => {
377 let mut utxo_manager = utxo_manager.lock().await;
379 utxo_manager.load_from_coins_vec(input_coins);
380 return Err(e);
381 }
382 };
383
384 let tx_id = tx.id(&consensus_parameters.chain_id());
385
386 let chain_id = consensus_parameters.chain_id();
387 let send_result = match self
388 .account
389 .send_transaction(chain_id, &tx, submit_clients)
390 .await
391 {
392 Ok(result) => result,
393 Err(e) => {
394 let err_msg = e.to_string();
395 if crate::wallet_ext::is_coin_invalid_error(&err_msg) {
396 tracing::warn!(
399 %tx_id,
400 "Transaction rejected due to invalid coins, \
401 not returning coins to UtxoManager: {err_msg}",
402 );
403 } else {
404 tracing::warn!(
407 %tx_id,
408 "Transaction failed, returning coins to UtxoManager: {err_msg}",
409 );
410 let mut utxo_manager = utxo_manager.lock().await;
411 utxo_manager.load_from_coins_vec(input_coins);
412 }
413 return Err(FuelsError::Other(format!(
414 "Failed to send transaction {tx_id}: {e}"
415 )));
416 }
417 };
418
419 maybe_return_coins(
422 &self.account,
423 &tx,
424 tx_id,
425 tx_config.expiration_height,
426 utxo_manager,
427 );
428
429 {
430 let mut utxo_manager = utxo_manager.lock().await;
431 utxo_manager.load_from_coins_vec(send_result.known_coins.clone());
432 utxo_manager.load_from_coins_vec(send_result.dynamic_coins.clone());
433 }
434
435 let receipts = receipts_of(&send_result.tx_status);
442
443 let failure_logs = match &send_result.tx_status {
444 TxStatus::Success(_)
445 | TxStatus::PreconfirmationSuccess(_)
446 | TxStatus::Submitted
447 | TxStatus::SqueezedOut(_) => None,
448 TxStatus::Failure(failure) | TxStatus::PreconfirmationFailure(failure) => {
449 let result = self.log_decoder.decode_logs(&failure.receipts);
450 tracing::error!(tx_id = %&send_result.tx_id, "Failed to process transaction: {result:?}");
451 Some(result)
452 }
453 };
454
455 let tx_status =
456 self.decode_response(send_result.tx_status)
457 .map_err(|e: FuelsError| {
458 if let Some(failure_logs) = &failure_logs {
459 FuelsError::Other(format!(
460 "Transaction {tx_id} failed with logs: {failure_logs:?} and error: {e}"
461 ))
462 } else {
463 FuelsError::Other(format!(
464 "Failed to get transaction status {tx_id}: {e}"
465 ))
466 }
467 });
468
469 let result = SendResult {
470 tx_id: send_result.tx_id,
471 tx_status,
472 known_coins: send_result.known_coins,
473 dynamic_coins: send_result.dynamic_coins,
474 preconf_rx_time: send_result.preconf_rx_time,
475 receipts,
476 };
477
478 Ok(result)
479 }
480}
481
482pub(crate) fn maybe_return_coins(
483 account: &Wallet,
484 tx: &Transaction,
485 tx_id: TxId,
486 expiration_height: Option<BlockHeight>,
487 utxo_manager: &SharedUtxoManager,
488) {
489 if let Some(expiration_height) = expiration_height {
490 let tx_inputs = tx.inputs().into_owned();
491 let provider = account.provider().clone();
492 let utxo_manager = utxo_manager.clone();
493
494 tokio::spawn(async move {
495 let target_height = expiration_height.succ().expect("shouldn't happen; qed");
497
498 let mut client = provider.client().clone();
506 match client
507 .with_required_fuel_block_height(Some(target_height))
508 .transaction(&tx_id)
509 .await
510 {
511 Ok(Some(tx_response)) => {
512 let status = tx_response.status;
514 match status {
515 TransactionStatus::Success { .. }
516 | TransactionStatus::Failure { .. } => {
517 tracing::debug!(
519 %tx_id,
520 "Transaction is confirmed/failed at height {}",
521 target_height
522 );
523 }
524 _ => {
525 tracing::warn!(
527 %tx_id,
528 "Transaction not confirmed/failed at height {target_height:?}, returning coins",
529 );
530 let coins = tx_inputs
531 .iter()
532 .filter_map(|input| FuelTxCoin::try_from(input).ok());
533 let mut utxo_manager = utxo_manager.lock().await;
534 utxo_manager.load_from_coins_vec(coins.collect());
535 }
536 }
537 }
538 Ok(None) => {
539 tracing::warn!(
542 %tx_id,
543 "Transaction not found at height {target_height:?}, \
544 not returning coins (may be invalid)",
545 );
546 }
547 Err(err) => {
548 tracing::error!(
549 %tx_id,
550 "Failed to get transaction status: {err:?} to return coins",
551 );
552 }
553 }
554 });
555 }
556}
557
558pub(crate) trait TransactionStatusExt {
559 fn missing_contracts_and_used_gas(&self) -> (HashSet<ContractId>, u64);
560}
561
562impl TransactionStatusExt for TransactionExecutionResult {
563 fn missing_contracts_and_used_gas(&self) -> (HashSet<ContractId>, u64) {
564 let contracts = find_ids_of_missing_contracts(self.receipts());
565 let used_gas = self
566 .receipts()
567 .iter()
568 .rfind(|r| matches!(r, Receipt::ScriptResult { .. }))
569 .map(|script_result| {
570 script_result
571 .gas_used()
572 .expect("could not retrieve gas used from ScriptResult")
573 })
574 .unwrap_or(0);
575
576 (contracts.into_iter().collect(), used_gas)
577 }
578}