use std::{collections::HashMap, io};
use chrono::{DateTime, Duration, Utc};
#[cfg(feature = "fuel-core")]
use fuel_core::service::{Config, FuelService};
use fuel_core_client::client::{
schema::{
balance::Balance, block::TimeParameters as FuelTimeParameters, contract::ContractBalance,
},
types::TransactionStatus,
FuelClient, PageDirection, PaginatedResult, PaginationRequest,
};
use fuel_tx::{AssetId, ConsensusParameters, Input, Receipt, TxPointer, UtxoId};
use fuel_types::MessageId;
use fuel_vm::state::ProgramState;
use fuels_types::{
bech32::{Bech32Address, Bech32ContractId},
block::Block,
chain_info::ChainInfo,
coin::Coin,
constants::{BASE_ASSET_ID, DEFAULT_GAS_ESTIMATION_TOLERANCE, MAX_GAS_PER_TX},
errors::{error, Error, Result},
message::Message,
message_proof::MessageProof,
node_info::NodeInfo,
resource::Resource,
transaction::Transaction,
transaction_response::TransactionResponse,
};
use itertools::Itertools;
use tai64::Tai64;
use thiserror::Error;
type ProviderResult<T> = std::result::Result<T, ProviderError>;
#[derive(Debug)]
pub struct TransactionCost {
pub min_gas_price: u64,
pub gas_price: u64,
pub gas_used: u64,
pub metered_bytes_size: u64,
pub total_fee: u64,
}
#[derive(Debug)]
pub struct TimeParameters {
pub start_time: DateTime<Utc>,
pub block_time_interval: Duration,
}
impl From<TimeParameters> for FuelTimeParameters {
fn from(time: TimeParameters) -> Self {
Self {
start_time: Tai64::from_unix(time.start_time.timestamp()).0.into(),
block_time_interval: (time.block_time_interval.num_seconds() as u64).into(),
}
}
}
pub(crate) struct ResourceQueries {
utxos: Vec<String>,
messages: Vec<String>,
asset_id: String,
amount: u64,
}
impl ResourceQueries {
pub fn new(
utxo_ids: Vec<UtxoId>,
message_ids: Vec<MessageId>,
asset_id: AssetId,
amount: u64,
) -> Self {
let utxos = utxo_ids
.iter()
.map(|utxo_id| format!("{utxo_id:#x}"))
.collect::<Vec<_>>();
let messages = message_ids
.iter()
.map(|msg_id| format!("{msg_id:#x}"))
.collect::<Vec<_>>();
Self {
utxos,
messages,
asset_id: format!("{asset_id:#x}"),
amount,
}
}
pub fn exclusion_query(&self) -> Option<(Vec<&str>, Vec<&str>)> {
if self.utxos.is_empty() && self.messages.is_empty() {
return None;
}
let utxos_as_str = self.utxos.iter().map(AsRef::as_ref).collect::<Vec<_>>();
let msg_ids_as_str = self.messages.iter().map(AsRef::as_ref).collect::<Vec<_>>();
Some((utxos_as_str, msg_ids_as_str))
}
pub fn spend_query(&self) -> Vec<(&str, u64, Option<u64>)> {
vec![(self.asset_id.as_str(), self.amount, None)]
}
}
pub struct ResourceFilter {
pub from: Bech32Address,
pub asset_id: AssetId,
pub amount: u64,
pub excluded_utxos: Vec<UtxoId>,
pub excluded_message_ids: Vec<MessageId>,
}
impl ResourceFilter {
pub fn owner(&self) -> String {
self.from.hash().to_string()
}
pub(crate) fn resource_queries(&self) -> ResourceQueries {
ResourceQueries::new(
self.excluded_utxos.clone(),
self.excluded_message_ids.clone(),
self.asset_id,
self.amount,
)
}
}
impl Default for ResourceFilter {
fn default() -> Self {
Self {
from: Default::default(),
asset_id: BASE_ASSET_ID,
amount: Default::default(),
excluded_utxos: Default::default(),
excluded_message_ids: Default::default(),
}
}
}
#[derive(Debug, Error)]
pub enum ProviderError {
#[error(transparent)]
ClientRequestError(#[from] io::Error),
}
impl From<ProviderError> for Error {
fn from(e: ProviderError) -> Self {
Error::ProviderError(e.to_string())
}
}
#[derive(Debug, Clone)]
pub struct Provider {
pub client: FuelClient,
}
impl Provider {
pub fn new(client: FuelClient) -> Self {
Self { client }
}
pub async fn send_transaction<T: Transaction + Clone>(&self, tx: &T) -> Result<Vec<Receipt>> {
let tolerance = 0.0;
let TransactionCost {
gas_used,
min_gas_price,
..
} = self.estimate_transaction_cost(tx, Some(tolerance)).await?;
if gas_used > tx.gas_limit() {
return Err(error!(
ProviderError,
"gas_limit({}) is lower than the estimated gas_used({})",
tx.gas_limit(),
gas_used
));
} else if min_gas_price > tx.gas_price() {
return Err(error!(
ProviderError,
"gas_price({}) is lower than the required min_gas_price({})",
tx.gas_price(),
min_gas_price
));
}
let chain_info = self.chain_info().await?;
tx.check_without_signatures(
chain_info.latest_block.header.height,
&chain_info.consensus_parameters,
)?;
let (status, receipts) = self.submit_with_feedback(tx.clone()).await?;
Self::if_failure_generate_error(&status, &receipts)?;
Ok(receipts)
}
fn if_failure_generate_error(status: &TransactionStatus, receipts: &[Receipt]) -> Result<()> {
if let TransactionStatus::Failure {
reason,
program_state,
..
} = status
{
let revert_id = program_state
.and_then(|state| match state {
ProgramState::Revert(revert_id) => Some(revert_id),
_ => None,
})
.expect("Transaction failed without a `revert_id`");
return Err(Error::RevertTransactionError {
reason: reason.to_string(),
revert_id,
receipts: receipts.to_owned(),
});
}
Ok(())
}
async fn submit_with_feedback(
&self,
tx: impl Transaction,
) -> ProviderResult<(TransactionStatus, Vec<Receipt>)> {
let tx_id = tx.id().to_string();
let status = self.client.submit_and_await_commit(&tx.into()).await?;
let receipts = self.client.receipts(&tx_id).await?;
Ok((status, receipts))
}
#[cfg(feature = "fuel-core")]
pub async fn launch(config: Config) -> Result<FuelClient> {
let srv = FuelService::new_node(config).await.unwrap();
Ok(FuelClient::from(srv.bound_address))
}
pub async fn connect(url: impl AsRef<str>) -> Result<Provider> {
let client = FuelClient::new(url).map_err(|err| error!(InfrastructureError, "{err}"))?;
Ok(Provider::new(client))
}
pub async fn chain_info(&self) -> ProviderResult<ChainInfo> {
Ok(self.client.chain_info().await?.into())
}
pub async fn consensus_parameters(&self) -> ProviderResult<ConsensusParameters> {
Ok(self.client.chain_info().await?.consensus_parameters.into())
}
pub async fn node_info(&self) -> ProviderResult<NodeInfo> {
Ok(self.client.node_info().await?.into())
}
pub async fn dry_run<T: Transaction + Clone>(&self, tx: &T) -> Result<Vec<Receipt>> {
let receipts = self.client.dry_run(&tx.clone().into()).await?;
Ok(receipts)
}
pub async fn dry_run_no_validation<T: Transaction + Clone>(
&self,
tx: &T,
) -> Result<Vec<Receipt>> {
let receipts = self
.client
.dry_run_opt(&tx.clone().into(), Some(false))
.await?;
Ok(receipts)
}
pub async fn get_coins(
&self,
from: &Bech32Address,
asset_id: AssetId,
) -> ProviderResult<Vec<Coin>> {
let mut coins: Vec<Coin> = vec![];
let mut cursor = None;
loop {
let res = self
.client
.coins(
&from.hash().to_string(),
Some(&asset_id.to_string()),
PaginationRequest {
cursor: cursor.clone(),
results: 100,
direction: PageDirection::Forward,
},
)
.await?;
if res.results.is_empty() {
break;
}
coins.extend(res.results.into_iter().map(Into::into));
cursor = res.cursor;
}
Ok(coins)
}
pub async fn get_spendable_resources(
&self,
filter: ResourceFilter,
) -> ProviderResult<Vec<Resource>> {
let queries = filter.resource_queries();
let res = self
.client
.resources_to_spend(
&filter.owner(),
queries.spend_query(),
queries.exclusion_query(),
)
.await?
.into_iter()
.flatten()
.map(|resource| {
resource
.try_into()
.map_err(ProviderError::ClientRequestError)
})
.try_collect()?;
Ok(res)
}
pub async fn get_asset_inputs(
&self,
filter: ResourceFilter,
witness_index: u8,
) -> Result<Vec<Input>> {
let asset_id = filter.asset_id;
Ok(self
.get_spendable_resources(filter)
.await?
.iter()
.map(|resource| match resource {
Resource::Coin(coin) => self.create_coin_input(coin, asset_id, witness_index),
Resource::Message(message) => self.create_message_input(message, witness_index),
})
.collect::<Vec<Input>>())
}
fn create_coin_input(&self, coin: &Coin, asset_id: AssetId, witness_index: u8) -> Input {
Input::coin_signed(
coin.utxo_id,
coin.owner.clone().into(),
coin.amount,
asset_id,
TxPointer::default(),
witness_index,
0,
)
}
fn create_message_input(&self, message: &Message, witness_index: u8) -> Input {
Input::message_signed(
message.message_id(),
message.sender.clone().into(),
message.recipient.clone().into(),
message.amount,
message.nonce,
witness_index,
message.data.clone(),
)
}
pub async fn get_asset_balance(
&self,
address: &Bech32Address,
asset_id: AssetId,
) -> ProviderResult<u64> {
self.client
.balance(&address.hash().to_string(), Some(&*asset_id.to_string()))
.await
.map_err(Into::into)
}
pub async fn get_contract_asset_balance(
&self,
contract_id: &Bech32ContractId,
asset_id: AssetId,
) -> ProviderResult<u64> {
self.client
.contract_balance(&contract_id.hash().to_string(), Some(&asset_id.to_string()))
.await
.map_err(Into::into)
}
pub async fn get_balances(
&self,
address: &Bech32Address,
) -> ProviderResult<HashMap<String, u64>> {
let pagination = PaginationRequest {
cursor: None,
results: 9999,
direction: PageDirection::Forward,
};
let balances_vec = self
.client
.balances(&address.hash().to_string(), pagination)
.await?
.results;
let balances = balances_vec
.into_iter()
.map(
|Balance {
owner: _,
amount,
asset_id,
}| (asset_id.to_string(), amount.try_into().unwrap()),
)
.collect();
Ok(balances)
}
pub async fn get_contract_balances(
&self,
contract_id: &Bech32ContractId,
) -> ProviderResult<HashMap<String, u64>> {
let pagination = PaginationRequest {
cursor: None,
results: 9999,
direction: PageDirection::Forward,
};
let balances_vec = self
.client
.contract_balances(&contract_id.hash().to_string(), pagination)
.await?
.results;
let balances = balances_vec
.into_iter()
.map(
|ContractBalance {
contract: _,
amount,
asset_id,
}| (asset_id.to_string(), amount.try_into().unwrap()),
)
.collect();
Ok(balances)
}
pub async fn get_transaction_by_id(
&self,
tx_id: &str,
) -> ProviderResult<Option<TransactionResponse>> {
Ok(self.client.transaction(tx_id).await?.map(Into::into))
}
pub async fn get_transactions(
&self,
request: PaginationRequest<String>,
) -> ProviderResult<PaginatedResult<TransactionResponse, String>> {
let pr = self.client.transactions(request).await?;
Ok(PaginatedResult {
cursor: pr.cursor,
results: pr.results.into_iter().map(Into::into).collect(),
has_next_page: pr.has_next_page,
has_previous_page: pr.has_previous_page,
})
}
pub async fn get_transactions_by_owner(
&self,
owner: &Bech32Address,
request: PaginationRequest<String>,
) -> ProviderResult<PaginatedResult<TransactionResponse, String>> {
let pr = self
.client
.transactions_by_owner(&owner.hash().to_string(), request)
.await?;
Ok(PaginatedResult {
cursor: pr.cursor,
results: pr.results.into_iter().map(Into::into).collect(),
has_next_page: pr.has_next_page,
has_previous_page: pr.has_previous_page,
})
}
pub async fn latest_block_height(&self) -> ProviderResult<u64> {
Ok(self.chain_info().await?.latest_block.header.height)
}
pub async fn latest_block_time(&self) -> ProviderResult<Option<DateTime<Utc>>> {
Ok(self.chain_info().await?.latest_block.header.time)
}
pub async fn produce_blocks(
&self,
amount: u64,
time: Option<TimeParameters>,
) -> io::Result<u64> {
let fuel_time: Option<FuelTimeParameters> = time.map(|t| t.into());
self.client.produce_blocks(amount, fuel_time).await
}
pub async fn block(&self, block_id: &str) -> ProviderResult<Option<Block>> {
let block = self.client.block(block_id).await?.map(Into::into);
Ok(block)
}
pub async fn get_blocks(
&self,
request: PaginationRequest<String>,
) -> ProviderResult<PaginatedResult<Block, String>> {
let pr = self.client.blocks(request).await?;
Ok(PaginatedResult {
cursor: pr.cursor,
results: pr.results.into_iter().map(Into::into).collect(),
has_next_page: pr.has_next_page,
has_previous_page: pr.has_previous_page,
})
}
pub async fn estimate_transaction_cost<T: Transaction + Clone>(
&self,
tx: &T,
tolerance: Option<f64>,
) -> Result<TransactionCost> {
let NodeInfo { min_gas_price, .. } = self.node_info().await?;
let tolerance = tolerance.unwrap_or(DEFAULT_GAS_ESTIMATION_TOLERANCE);
let dry_run_tx = Self::generate_dry_run_tx(tx);
let consensus_parameters = self.chain_info().await?.consensus_parameters;
let gas_used = self
.get_gas_used_with_tolerance(&dry_run_tx, tolerance)
.await?;
let gas_price = std::cmp::max(tx.gas_price(), min_gas_price);
dry_run_tx
.with_gas_price(gas_price)
.with_gas_limit(gas_used);
let transaction_fee = tx
.fee_checked_from_tx(&consensus_parameters)
.expect("Error calculating TransactionFee");
Ok(TransactionCost {
min_gas_price,
gas_price,
gas_used,
metered_bytes_size: tx.metered_bytes_size() as u64,
total_fee: transaction_fee.total(),
})
}
fn generate_dry_run_tx<T: Transaction + Clone>(tx: &T) -> T {
tx.clone().with_gas_limit(MAX_GAS_PER_TX).with_gas_price(0)
}
async fn get_gas_used_with_tolerance<T: Transaction + Clone>(
&self,
tx: &T,
tolerance: f64,
) -> Result<u64> {
let gas_used = self.get_gas_used(&self.dry_run_no_validation(tx).await?);
Ok((gas_used as f64 * (1.0 + tolerance)) as u64)
}
fn get_gas_used(&self, receipts: &[Receipt]) -> u64 {
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)
}
pub async fn get_messages(&self, from: &Bech32Address) -> ProviderResult<Vec<Message>> {
let pagination = PaginationRequest {
cursor: None,
results: 100,
direction: PageDirection::Forward,
};
let res = self
.client
.messages(Some(&from.hash().to_string()), pagination)
.await?
.results
.into_iter()
.map(Into::into)
.collect();
Ok(res)
}
pub async fn get_message_proof(
&self,
tx_id: &str,
message_id: &str,
) -> ProviderResult<Option<MessageProof>> {
let proof = self
.client
.message_proof(tx_id, message_id)
.await?
.map(Into::into);
Ok(proof)
}
}