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