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 let chain_id = consensus_parameters.chain_id();
169
170 let max_fee = builder_date.max_fee();
171
172 let input_coins = {
173 let mut utxo_manager = utxo_manager.lock().await;
174 utxo_manager
175 .guaranteed_extract_coins(owner, base_asset_id, max_fee as u128)
176 .map_err(|e| FuelsError::Other(e.to_string()))
177 }?;
178 let coins_iter = input_coins.iter();
179 let account = self.account.clone();
180
181 let assemble_tx = async move {
182 let witness_limit = crate::wallet_ext::SIGNATURE_MARGIN;
183
184 let mut builder =
185 fuel_core_types::fuel_tx::TransactionBuilder::<Script>::script(
186 tb.script,
187 tb.script_data,
188 );
189 builder
190 .with_chain_id(consensus_parameters.chain_id())
191 .max_fee_limit(max_fee)
192 .witness_limit(witness_limit as u64);
193
194 if let Some(expiration_height) = tx_config.expiration_height {
195 builder.expiration(expiration_height);
196 }
197
198 for coin in coins_iter {
199 builder.add_unsigned_coin_input(
200 secret_key,
201 coin.utxo_id,
202 coin.amount,
203 coin.asset_id,
204 TxPointer::default(),
205 );
206 }
207
208 builder.add_output(Output::Change {
209 to: owner,
210 amount: 0,
211 asset_id: base_asset_id,
212 });
213
214 for input in tb.inputs {
215 if let fuels::types::input::Input::Contract { contract_id, .. } = input {
216 let contract_index = builder.inputs().len();
217 builder.add_input(Input::contract(
218 Default::default(),
219 Default::default(),
220 Default::default(),
221 Default::default(),
222 contract_id,
223 ));
224 builder.add_output(Output::contract(
225 contract_index as u16,
226 Default::default(),
227 Default::default(),
228 ));
229 }
230 }
231
232 if let VariableOutputPolicy::Exactly(variable_outputs) =
234 tb.variable_output_policy
235 {
236 for _ in 0..variable_outputs {
237 builder.add_output(Output::Variable {
238 to: Default::default(),
239 amount: 0,
240 asset_id: Default::default(),
241 });
242 }
243 }
244
245 let dummy_script = builder.clone().finalize();
246 let max_gas = dummy_script.max_gas(
247 consensus_parameters.gas_costs(),
248 consensus_parameters.fee_params(),
249 ) + 1;
250 let available_gas =
251 consensus_parameters.tx_params().max_gas_per_tx() - max_gas;
252
253 let (missing_contracts, used_gas) = if tx_config.estimate_gas_usage {
254 builder.script_gas_limit(available_gas);
255
256 let client = account.provider().client();
257 let tx_to_dry_run = builder.clone().finalize().into();
258
259 let result = client
260 .dry_run_opt(
261 &[tx_to_dry_run],
262 Some(false),
263 Some(builder_date.gas_price),
264 None,
265 )
266 .await?
267 .into_iter()
268 .next()
269 .ok_or_else(|| {
270 FuelsError::Other("Dry run failed to return a result".to_string())
271 })?;
272
273 result.result.missing_contracts_and_used_gas()
274 } else {
275 (Default::default(), 0)
276 };
277
278 for contract_id in missing_contracts {
279 let contract_index = builder.inputs().len();
280 builder.add_input(Input::contract(
281 Default::default(),
282 Default::default(),
283 Default::default(),
284 Default::default(),
285 contract_id,
286 ));
287
288 builder.add_output(Output::contract(
289 contract_index as u16,
290 Default::default(),
291 Default::default(),
292 ));
293 }
294
295 let gas_limit = std::cmp::max(
296 tx_config.min_gas_limit,
297 std::cmp::min(used_gas * 2 + 100_000, available_gas),
298 );
299 builder.script_gas_limit(gas_limit);
300
301 Ok(builder.finalize_as_transaction())
302 };
303
304 let tx = match assemble_tx.await {
305 Ok(tx) => tx,
306 Err(e) => {
307 let mut utxo_manager = utxo_manager.lock().await;
309 utxo_manager.load_from_coins_vec(input_coins);
310 return Err(e);
311 }
312 };
313
314 let tx_id = tx.id(&consensus_parameters.chain_id());
315
316 maybe_return_coins(
317 &self.account,
318 &tx,
319 tx_id,
320 tx_config.expiration_height,
321 utxo_manager,
322 );
323
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}