use crate::{
order_book::DEFAULT_METHOD_GAS,
utxo_manager::{
FuelTxCoin,
SharedUtxoManager,
},
wallet_ext::{
BuilderData,
SendResult,
WalletExt,
},
};
use fuel_core_client::client::{
FuelClient,
types::TransactionStatus,
};
use fuel_core_types::{
blockchain::transaction::TransactionExt,
fuel_tx::{
Chargeable,
Finalizable,
Input,
Output,
Receipt,
Script,
Transaction,
TxId,
TxPointer,
UniqueIdentifier,
},
fuel_types::ContractId,
services::executor::TransactionExecutionResult,
};
use fuels::{
accounts::ViewOnlyAccount,
core::traits::{
Parameterize,
Tokenizable,
},
prelude::{
CallHandler,
Wallet,
},
programs::{
calls::{
traits::{
ContractDependencyConfigurator,
ResponseParser,
TransactionTuner,
},
utils::find_ids_of_missing_contracts,
},
responses::CallResponse,
},
types::{
BlockHeight,
errors::{
Error as FuelsError,
Result as FuelsResult,
},
transaction_builders::VariableOutputPolicy,
tx_status::TxStatus,
},
};
use std::{
collections::HashSet,
fmt::Debug,
future::Future,
};
pub trait CallHandlerExt<T> {
fn almost_sync_call(
self,
builder_date: &BuilderData,
utxo_manager: &SharedUtxoManager,
tx_config: &Option<TransactionConfig>,
) -> impl Future<Output = FuelsResult<SendResult<FuelsResult<CallResponse<T>>>>>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct TransactionConfig {
pub min_gas_limit: u64,
pub estimate_gas_usage: bool,
pub expiration_height: Option<BlockHeight>,
}
impl TransactionConfig {
pub fn builder() -> TransactionConfigBuilder {
TransactionConfigBuilder::new()
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct TransactionConfigBuilder {
min_gas_limit: Option<u64>,
estimate_gas_usage: Option<bool>,
expiration_height: Option<BlockHeight>,
}
impl TransactionConfigBuilder {
pub fn new() -> Self {
TransactionConfigBuilder {
min_gas_limit: None,
estimate_gas_usage: None,
expiration_height: None,
}
}
pub fn with_min_gas_limit(&mut self, min_gas_limit: u64) -> &mut Self {
self.min_gas_limit = Some(min_gas_limit);
self
}
pub fn with_estimate_gas_usage(&mut self, estimate_gas_usage: bool) -> &mut Self {
self.estimate_gas_usage = Some(estimate_gas_usage);
self
}
pub fn min_gas_limit(&self) -> Option<u64> {
self.min_gas_limit
}
pub fn estimate_gas_usage(&self) -> Option<bool> {
self.estimate_gas_usage
}
pub fn with_expiration_height(
&mut self,
expiration_height: BlockHeight,
) -> &mut Self {
self.expiration_height = Some(expiration_height);
self
}
pub fn expiration_height(&self) -> Option<BlockHeight> {
self.expiration_height
}
pub fn build(self) -> TransactionConfig {
TransactionConfig {
min_gas_limit: self.min_gas_limit.unwrap_or(DEFAULT_METHOD_GAS),
estimate_gas_usage: self.estimate_gas_usage.unwrap_or(true),
expiration_height: self.expiration_height,
}
}
}
impl<C, T> CallHandlerExt<T> for CallHandler<Wallet, C, T>
where
C: ContractDependencyConfigurator + TransactionTuner + ResponseParser,
T: Tokenizable + Parameterize + Debug,
{
#[tracing::instrument(skip_all)]
async fn almost_sync_call(
self,
builder_date: &BuilderData,
utxo_manager: &SharedUtxoManager,
tx_config: &Option<TransactionConfig>,
) -> FuelsResult<SendResult<FuelsResult<CallResponse<T>>>> {
let tx_config = tx_config.unwrap_or_default();
let consensus_parameters = &builder_date.consensus_parameters;
let tb =
self.transaction_builder_with_parameters(consensus_parameters, vec![])?;
let owner = self.account.address();
let secret_key = self.account.signer().secret_key();
let base_asset_id = *consensus_parameters.base_asset_id();
let chain_id = consensus_parameters.chain_id();
let max_fee = builder_date.max_fee();
let input_coins = {
let mut utxo_manager = utxo_manager.lock().await;
utxo_manager
.guaranteed_extract_coins(owner, base_asset_id, max_fee as u128)
.map_err(|e| FuelsError::Other(e.to_string()))
}?;
let coins_iter = input_coins.iter();
let account = self.account.clone();
let assemble_tx = async move {
let witness_limit = crate::wallet_ext::SIGNATURE_MARGIN;
let mut builder =
fuel_core_types::fuel_tx::TransactionBuilder::<Script>::script(
tb.script,
tb.script_data,
);
builder
.with_chain_id(consensus_parameters.chain_id())
.max_fee_limit(max_fee)
.witness_limit(witness_limit as u64);
if let Some(expiration_height) = tx_config.expiration_height {
builder.expiration(expiration_height);
}
for coin in coins_iter {
builder.add_unsigned_coin_input(
secret_key,
coin.utxo_id,
coin.amount,
coin.asset_id,
TxPointer::default(),
);
}
builder.add_output(Output::Change {
to: owner,
amount: 0,
asset_id: base_asset_id,
});
for input in tb.inputs {
if let fuels::types::input::Input::Contract { contract_id, .. } = input {
let contract_index = builder.inputs().len();
builder.add_input(Input::contract(
Default::default(),
Default::default(),
Default::default(),
Default::default(),
contract_id,
));
builder.add_output(Output::contract(
contract_index as u16,
Default::default(),
Default::default(),
));
}
}
if let VariableOutputPolicy::Exactly(variable_outputs) =
tb.variable_output_policy
{
for _ in 0..variable_outputs {
builder.add_output(Output::Variable {
to: Default::default(),
amount: 0,
asset_id: Default::default(),
});
}
}
let dummy_script = builder.clone().finalize();
let max_gas = dummy_script.max_gas(
consensus_parameters.gas_costs(),
consensus_parameters.fee_params(),
) + 1;
let available_gas =
consensus_parameters.tx_params().max_gas_per_tx() - max_gas;
let (missing_contracts, used_gas) = if tx_config.estimate_gas_usage {
builder.script_gas_limit(available_gas);
let client = account.provider().client();
let tx_to_dry_run = builder.clone().finalize().into();
let result = client
.dry_run_opt(
&[tx_to_dry_run],
Some(false),
Some(builder_date.gas_price),
None,
)
.await?
.into_iter()
.next()
.ok_or_else(|| {
FuelsError::Other("Dry run failed to return a result".to_string())
})?;
result.result.missing_contracts_and_used_gas()
} else {
(Default::default(), 0)
};
for contract_id in missing_contracts {
let contract_index = builder.inputs().len();
builder.add_input(Input::contract(
Default::default(),
Default::default(),
Default::default(),
Default::default(),
contract_id,
));
builder.add_output(Output::contract(
contract_index as u16,
Default::default(),
Default::default(),
));
}
let gas_limit = std::cmp::max(
tx_config.min_gas_limit,
std::cmp::min(used_gas * 2 + 100_000, available_gas),
);
builder.script_gas_limit(gas_limit);
Ok(builder.finalize_as_transaction())
};
let tx = match assemble_tx.await {
Ok(tx) => tx,
Err(e) => {
let mut utxo_manager = utxo_manager.lock().await;
utxo_manager.load_from_coins_vec(input_coins);
return Err(e);
}
};
let tx_id = tx.id(&consensus_parameters.chain_id());
maybe_return_coins(
&self.account,
&tx,
tx_id,
tx_config.expiration_height,
utxo_manager,
);
let send_result =
self.account
.send_transaction(chain_id, &tx)
.await
.map_err(|e| {
FuelsError::Other(format!("Failed to send transaction {tx_id}: {e}"))
})?;
{
let mut utxo_manager = utxo_manager.lock().await;
utxo_manager.load_from_coins_vec(send_result.known_coins.clone());
utxo_manager.load_from_coins_vec(send_result.dynamic_coins.clone());
}
let failure_logs = match &send_result.tx_status {
TxStatus::Success(_)
| TxStatus::PreconfirmationSuccess(_)
| TxStatus::Submitted
| TxStatus::SqueezedOut(_) => None,
TxStatus::Failure(failure) | TxStatus::PreconfirmationFailure(failure) => {
let result = self.log_decoder.decode_logs(&failure.receipts);
tracing::error!(tx_id = %&send_result.tx_id, "Failed to process transaction: {result:?}");
Some(result)
}
};
let tx_status =
self.get_response(send_result.tx_status)
.map_err(|e: FuelsError| {
if let Some(failure_logs) = &failure_logs {
FuelsError::Other(format!(
"Transaction {tx_id} failed with logs: {failure_logs:?} and error: {e}"
))
} else {
FuelsError::Other(format!(
"Failed to get transaction status {tx_id}: {e}"
))
}
});
let result = SendResult {
tx_id: send_result.tx_id,
tx_status,
known_coins: send_result.known_coins,
dynamic_coins: send_result.dynamic_coins,
preconf_rx_time: send_result.preconf_rx_time,
};
Ok(result)
}
}
pub(crate) fn maybe_return_coins(
account: &Wallet,
tx: &Transaction,
tx_id: TxId,
expiration_height: Option<BlockHeight>,
utxo_manager: &SharedUtxoManager,
) {
if let Some(expiration_height) = expiration_height {
let tx_inputs = tx.inputs().into_owned();
let provider = account.provider().clone();
let utxo_manager = utxo_manager.clone();
tokio::spawn(async move {
let target_height = expiration_height.succ().expect("shouldn't happen; qed");
let mut client = FuelClient::new(provider.url())
.expect("The URL is correct because we send transactions before; qed");
match client
.with_required_fuel_block_height(Some(target_height))
.transaction(&tx_id)
.await
{
Ok(Some(tx_response)) => {
let status = tx_response.status;
match status {
TransactionStatus::Success { .. }
| TransactionStatus::Failure { .. } => {
tracing::debug!(
%tx_id,
"Transaction is confirmed/failed at height {}",
target_height
);
}
_ => {
tracing::warn!(
%tx_id,
"Transaction not confirmed/failed at height {target_height:?}, returning coins",
);
let coins = tx_inputs
.iter()
.filter_map(|input| FuelTxCoin::try_from(input).ok());
let mut utxo_manager = utxo_manager.lock().await;
utxo_manager.load_from_coins_vec(coins.collect());
}
}
}
Ok(None) => {
tracing::warn!(
%tx_id,
"Transaction not found at height {target_height:?}, returning coins",
);
let coins = tx_inputs
.iter()
.filter_map(|input| FuelTxCoin::try_from(input).ok());
let mut utxo_manager = utxo_manager.lock().await;
utxo_manager.load_from_coins_vec(coins.collect());
}
Err(err) => {
tracing::error!(
%tx_id,
"Failed to get transaction status: {err:?} to return coins",
);
}
}
});
}
}
pub(crate) trait TransactionStatusExt {
fn missing_contracts_and_used_gas(&self) -> (HashSet<ContractId>, u64);
}
impl TransactionStatusExt for TransactionExecutionResult {
fn missing_contracts_and_used_gas(&self) -> (HashSet<ContractId>, u64) {
let contracts = find_ids_of_missing_contracts(self.receipts());
let used_gas = self
.receipts()
.iter()
.rfind(|r| matches!(r, Receipt::ScriptResult { .. }))
.map(|script_result| {
script_result
.gas_used()
.expect("could not retrieve gas used from ScriptResult")
})
.unwrap_or(0);
(contracts.into_iter().collect(), used_gas)
}
}