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