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