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 maybe_return_coins(
316 &self.account,
317 &tx,
318 tx_id,
319 tx_config.expiration_height,
320 utxo_manager,
321 );
322
323 let chain_id = consensus_parameters.chain_id();
324 let send_result =
325 self.account
326 .send_transaction(chain_id, &tx)
327 .await
328 .map_err(|e| {
329 FuelsError::Other(format!("Failed to send transaction {tx_id}: {e}"))
330 })?;
331
332 {
333 let mut utxo_manager = utxo_manager.lock().await;
334 utxo_manager.load_from_coins_vec(send_result.known_coins.clone());
335 utxo_manager.load_from_coins_vec(send_result.dynamic_coins.clone());
336 }
337
338 let failure_logs = match &send_result.tx_status {
339 TxStatus::Success(_)
340 | TxStatus::PreconfirmationSuccess(_)
341 | TxStatus::Submitted
342 | TxStatus::SqueezedOut(_) => None,
343 TxStatus::Failure(failure) | TxStatus::PreconfirmationFailure(failure) => {
344 let result = self.log_decoder.decode_logs(&failure.receipts);
345 tracing::error!(tx_id = %&send_result.tx_id, "Failed to process transaction: {result:?}");
346 Some(result)
347 }
348 };
349
350 let tx_status =
351 self.get_response(send_result.tx_status)
352 .map_err(|e: FuelsError| {
353 if let Some(failure_logs) = &failure_logs {
354 FuelsError::Other(format!(
355 "Transaction {tx_id} failed with logs: {failure_logs:?} and error: {e}"
356 ))
357 } else {
358 FuelsError::Other(format!(
359 "Failed to get transaction status {tx_id}: {e}"
360 ))
361 }
362 });
363
364 let result = SendResult {
365 tx_id: send_result.tx_id,
366 tx_status,
367 known_coins: send_result.known_coins,
368 dynamic_coins: send_result.dynamic_coins,
369 preconf_rx_time: send_result.preconf_rx_time,
370 };
371
372 Ok(result)
373 }
374}
375
376pub(crate) fn maybe_return_coins(
377 account: &Wallet,
378 tx: &Transaction,
379 tx_id: TxId,
380 expiration_height: Option<BlockHeight>,
381 utxo_manager: &SharedUtxoManager,
382) {
383 if let Some(expiration_height) = expiration_height {
384 let tx_inputs = tx.inputs().into_owned();
385 let provider = account.provider().clone();
386 let utxo_manager = utxo_manager.clone();
387
388 tokio::spawn(async move {
389 let target_height = expiration_height.succ().expect("shouldn't happen; qed");
391
392 let mut client = FuelClient::new(provider.url())
394 .expect("The URL is correct because we send transactions before; qed");
395 match client
396 .with_required_fuel_block_height(Some(target_height))
397 .transaction(&tx_id)
398 .await
399 {
400 Ok(Some(tx_response)) => {
401 let status = tx_response.status;
403 match status {
404 TransactionStatus::Success { .. }
405 | TransactionStatus::Failure { .. } => {
406 tracing::debug!(
408 %tx_id,
409 "Transaction is confirmed/failed at height {}",
410 target_height
411 );
412 }
413 _ => {
414 tracing::warn!(
416 %tx_id,
417 "Transaction not confirmed/failed at height {target_height:?}, returning coins",
418 );
419 let coins = tx_inputs
420 .iter()
421 .filter_map(|input| FuelTxCoin::try_from(input).ok());
422 let mut utxo_manager = utxo_manager.lock().await;
423 utxo_manager.load_from_coins_vec(coins.collect());
424 }
425 }
426 }
427 Ok(None) => {
428 tracing::warn!(
430 %tx_id,
431 "Transaction not found at height {target_height:?}, returning coins",
432 );
433 let coins = tx_inputs
434 .iter()
435 .filter_map(|input| FuelTxCoin::try_from(input).ok());
436 let mut utxo_manager = utxo_manager.lock().await;
437 utxo_manager.load_from_coins_vec(coins.collect());
438 }
439 Err(err) => {
440 tracing::error!(
441 %tx_id,
442 "Failed to get transaction status: {err:?} to return coins",
443 );
444 }
445 }
446 });
447 }
448}
449
450pub(crate) trait TransactionStatusExt {
451 fn missing_contracts_and_used_gas(&self) -> (HashSet<ContractId>, u64);
452}
453
454impl TransactionStatusExt for TransactionExecutionResult {
455 fn missing_contracts_and_used_gas(&self) -> (HashSet<ContractId>, u64) {
456 let contracts = find_ids_of_missing_contracts(self.receipts());
457 let used_gas = self
458 .receipts()
459 .iter()
460 .rfind(|r| matches!(r, Receipt::ScriptResult { .. }))
461 .map(|script_result| {
462 script_result
463 .gas_used()
464 .expect("could not retrieve gas used from ScriptResult")
465 })
466 .unwrap_or(0);
467
468 (contracts.into_iter().collect(), used_gas)
469 }
470}