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