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