use crate::{
call_response::FuelCallResponse,
execution_script::ExecutableFuelCall,
logs::{decode_revert_error, LogDecoder},
};
use fuel_gql_client::{
fuel_tx::{Contract as FuelContract, Output, Receipt, StorageSlot, Transaction},
prelude::PanicReason,
};
use fuel_tx::{Address, AssetId, Checkable, Create, Salt};
use fuels_core::{
abi_decoder::ABIDecoder,
abi_encoder::{ABIEncoder, UnresolvedBytes},
constants::FAILED_TRANSFER_TO_ADDRESS_SIGNAL,
parameters::StorageConfiguration,
parameters::{CallParameters, TxParameters},
tx::{Bytes32, ContractId},
Parameterize, Selector, Token, Tokenizable,
};
use fuels_signers::{
provider::{Provider, TransactionCost},
Signer, WalletUnlocked,
};
use fuels_types::{
bech32::Bech32ContractId,
errors::Error,
param_types::{ParamType, ReturnLocation},
};
use std::{
collections::{HashMap, HashSet},
fmt::Debug,
fs,
marker::PhantomData,
panic,
path::Path,
str::FromStr,
};
pub const DEFAULT_TX_DEP_ESTIMATION_ATTEMPTS: u64 = 10;
#[derive(Debug, Clone, Default)]
pub struct CompiledContract {
pub raw: Vec<u8>,
pub salt: Salt,
pub storage_slots: Vec<StorageSlot>,
}
pub struct Contract {
pub compiled_contract: CompiledContract,
pub wallet: WalletUnlocked,
}
impl Contract {
pub fn new(compiled_contract: CompiledContract, wallet: WalletUnlocked) -> Self {
Self {
compiled_contract,
wallet,
}
}
pub fn compute_contract_id_and_state_root(
compiled_contract: &CompiledContract,
) -> (ContractId, Bytes32) {
let fuel_contract = FuelContract::from(compiled_contract.raw.clone());
let root = fuel_contract.root();
let state_root = FuelContract::initial_state_root(compiled_contract.storage_slots.iter());
let contract_id = fuel_contract.id(&compiled_contract.salt, &root, &state_root);
(contract_id, state_root)
}
pub fn method_hash<D: Tokenizable + Parameterize + Debug>(
provider: &Provider,
contract_id: Bech32ContractId,
wallet: &WalletUnlocked,
signature: Selector,
args: &[Token],
log_decoder: LogDecoder,
) -> Result<ContractCallHandler<D>, Error> {
let encoded_selector = signature;
let tx_parameters = TxParameters::default();
let call_parameters = CallParameters::default();
let compute_custom_input_offset = Contract::should_compute_custom_input_offset(args);
let unresolved_bytes = ABIEncoder::encode(args)?;
let contract_call = ContractCall {
contract_id,
encoded_selector,
encoded_args: unresolved_bytes,
call_parameters,
compute_custom_input_offset,
variable_outputs: None,
message_outputs: None,
external_contracts: vec![],
output_param: D::param_type(),
};
Ok(ContractCallHandler {
contract_call,
tx_parameters,
wallet: wallet.clone(),
provider: provider.clone(),
datatype: PhantomData,
log_decoder,
})
}
fn should_compute_custom_input_offset(args: &[Token]) -> bool {
args.len() > 1
|| args.iter().any(|t| {
matches!(
t,
Token::String(_)
| Token::Struct(_)
| Token::Enum(_)
| Token::B256(_)
| Token::Tuple(_)
| Token::Array(_)
| Token::Byte(_)
| Token::Vector(_)
)
})
}
pub async fn deploy(
binary_filepath: &str,
wallet: &WalletUnlocked,
params: TxParameters,
storage_configuration: StorageConfiguration,
) -> Result<Bech32ContractId, Error> {
let mut compiled_contract =
Contract::load_contract(binary_filepath, &storage_configuration.storage_path)?;
Self::merge_storage_vectors(&storage_configuration, &mut compiled_contract);
Self::deploy_loaded(&(compiled_contract), wallet, params).await
}
pub async fn deploy_with_parameters(
binary_filepath: &str,
wallet: &WalletUnlocked,
params: TxParameters,
storage_configuration: StorageConfiguration,
salt: Salt,
) -> Result<Bech32ContractId, Error> {
let mut compiled_contract = Contract::load_contract_with_parameters(
binary_filepath,
&storage_configuration.storage_path,
salt,
)?;
Self::merge_storage_vectors(&storage_configuration, &mut compiled_contract);
Self::deploy_loaded(&(compiled_contract), wallet, params).await
}
fn merge_storage_vectors(
storage_configuration: &StorageConfiguration,
compiled_contract: &mut CompiledContract,
) {
match &storage_configuration.manual_storage_vec {
Some(storage) if !storage.is_empty() => {
compiled_contract.storage_slots =
Self::merge_storage_slots(storage, &compiled_contract.storage_slots);
}
_ => {}
}
}
pub async fn deploy_loaded(
compiled_contract: &CompiledContract,
wallet: &WalletUnlocked,
params: TxParameters,
) -> Result<Bech32ContractId, Error> {
let (mut tx, contract_id) =
Self::contract_deployment_transaction(compiled_contract, params).await?;
wallet.add_fee_coins(&mut tx, 0, 1).await?;
wallet.sign_transaction(&mut tx).await?;
let provider = wallet.get_provider()?;
let chain_info = provider.chain_info().await?;
tx.check_without_signatures(
chain_info.latest_block.header.height,
&chain_info.consensus_parameters,
)?;
provider.send_transaction(&tx).await?;
Ok(contract_id)
}
pub fn load_contract(
binary_filepath: &str,
storage_path: &Option<String>,
) -> Result<CompiledContract, Error> {
Self::load_contract_with_parameters(binary_filepath, storage_path, Salt::from([0u8; 32]))
}
pub fn load_contract_with_parameters(
binary_filepath: &str,
storage_path: &Option<String>,
salt: Salt,
) -> Result<CompiledContract, Error> {
let extension = Path::new(binary_filepath).extension().unwrap();
if extension != "bin" {
return Err(Error::InvalidData(extension.to_str().unwrap().to_owned()));
}
let bin = std::fs::read(binary_filepath)?;
let storage = match storage_path {
Some(path) if Path::new(&path).exists() => Self::get_storage_vec(path),
Some(path) if !Path::new(&path).exists() => {
return Err(Error::InvalidData(path.to_owned()));
}
_ => {
vec![]
}
};
Ok(CompiledContract {
raw: bin,
salt,
storage_slots: storage,
})
}
fn merge_storage_slots(
manual_storage: &[StorageSlot],
contract_storage: &[StorageSlot],
) -> Vec<StorageSlot> {
let mut return_storage: Vec<StorageSlot> = manual_storage.to_owned();
let keys: HashSet<Bytes32> = manual_storage.iter().map(|slot| *slot.key()).collect();
contract_storage.iter().for_each(|slot| {
if !keys.contains(slot.key()) {
return_storage.push(slot.clone())
}
});
return_storage
}
pub async fn contract_deployment_transaction(
compiled_contract: &CompiledContract,
params: TxParameters,
) -> Result<(Create, Bech32ContractId), Error> {
let bytecode_witness_index = 0;
let storage_slots: Vec<StorageSlot> = compiled_contract.storage_slots.clone();
let witnesses = vec![compiled_contract.raw.clone().into()];
let (contract_id, state_root) = Self::compute_contract_id_and_state_root(compiled_contract);
let outputs = vec![Output::contract_created(contract_id, state_root)];
let tx = Transaction::create(
params.gas_price,
params.gas_limit,
params.maturity,
bytecode_witness_index,
compiled_contract.salt,
storage_slots,
vec![],
outputs,
witnesses,
);
Ok((tx, contract_id.into()))
}
fn get_storage_vec(storage_path: &str) -> Vec<StorageSlot> {
let mut return_storage: Vec<StorageSlot> = vec![];
let storage_json_string = fs::read_to_string(storage_path).expect("Unable to read file");
let storage: serde_json::Value = serde_json::from_str(storage_json_string.as_str())
.expect("JSON was not well-formatted");
for slot in storage.as_array().unwrap() {
return_storage.push(StorageSlot::new(
Bytes32::from_str(slot["key"].as_str().unwrap()).unwrap(),
Bytes32::from_str(slot["value"].as_str().unwrap()).unwrap(),
));
}
return_storage
}
}
#[derive(Debug)]
pub struct ContractCall {
pub contract_id: Bech32ContractId,
pub encoded_args: UnresolvedBytes,
pub encoded_selector: Selector,
pub call_parameters: CallParameters,
pub compute_custom_input_offset: bool,
pub variable_outputs: Option<Vec<Output>>,
pub message_outputs: Option<Vec<Output>>,
pub external_contracts: Vec<Bech32ContractId>,
pub output_param: ParamType,
}
impl ContractCall {
pub fn with_contract_id(self, contract_id: Bech32ContractId) -> Self {
ContractCall {
contract_id,
..self
}
}
pub fn with_external_contracts(
self,
external_contracts: Vec<Bech32ContractId>,
) -> ContractCall {
ContractCall {
external_contracts,
..self
}
}
pub fn with_variable_outputs(self, variable_outputs: Vec<Output>) -> ContractCall {
ContractCall {
variable_outputs: Some(variable_outputs),
..self
}
}
pub fn with_message_outputs(self, message_outputs: Vec<Output>) -> ContractCall {
ContractCall {
message_outputs: Some(message_outputs),
..self
}
}
pub fn with_call_parameters(self, call_parameters: CallParameters) -> ContractCall {
ContractCall {
call_parameters,
..self
}
}
pub fn append_variable_outputs(&mut self, num: u64) {
let new_variable_outputs = vec![
Output::Variable {
amount: 0,
to: Address::zeroed(),
asset_id: AssetId::default(),
};
num as usize
];
match self.variable_outputs {
Some(ref mut outputs) => outputs.extend(new_variable_outputs),
None => self.variable_outputs = Some(new_variable_outputs),
}
}
pub fn append_external_contracts(&mut self, contract_id: Bech32ContractId) {
self.external_contracts.push(contract_id)
}
pub fn append_message_outputs(&mut self, num: u64) {
let new_message_outputs = vec![
Output::Message {
recipient: Address::zeroed(),
amount: 0,
};
num as usize
];
match self.message_outputs {
Some(ref mut outputs) => outputs.extend(new_message_outputs),
None => self.message_outputs = Some(new_message_outputs),
}
}
fn is_missing_output_variables(receipts: &[Receipt]) -> bool {
receipts.iter().any(
|r| matches!(r, Receipt::Revert { ra, .. } if *ra == FAILED_TRANSFER_TO_ADDRESS_SIGNAL),
)
}
fn find_contract_not_in_inputs(receipts: &[Receipt]) -> Option<&Receipt> {
receipts.iter().find(
|r| matches!(r, Receipt::Panic { reason, .. } if *reason.reason() == PanicReason::ContractNotInInputs ),
)
}
}
pub fn get_decoded_output(
receipts: &mut Vec<Receipt>,
contract_id: Option<&Bech32ContractId>,
output_param: &ParamType,
) -> Result<Token, Error> {
let contract_id: ContractId = match contract_id {
Some(contract_id) => contract_id.into(),
None => ContractId::new([0u8; 32]),
};
let (encoded_value, index) = match output_param.get_return_location() {
ReturnLocation::ReturnData => {
match receipts.iter().position(|receipt| {
matches!(receipt,
Receipt::ReturnData { id, data, .. } if *id == contract_id && !data.is_empty())
}) {
Some(idx) => (
receipts[idx]
.data()
.expect("ReturnData should have data")
.to_vec(),
Some(idx),
),
None => (vec![], None),
}
}
ReturnLocation::Return => {
match receipts.iter().position(|receipt| {
matches!(receipt,
Receipt::Return { id, ..} if *id == contract_id)
}) {
Some(idx) => (
receipts[idx]
.val()
.expect("Return should have val")
.to_be_bytes()
.to_vec(),
Some(idx),
),
None => (vec![], None),
}
}
};
if let Some(i) = index {
receipts.remove(i);
}
let decoded_value = ABIDecoder::decode_single(output_param, &encoded_value)?;
Ok(decoded_value)
}
#[derive(Debug)]
#[must_use = "contract calls do nothing unless you `call` them"]
pub struct ContractCallHandler<D> {
pub contract_call: ContractCall,
pub tx_parameters: TxParameters,
pub wallet: WalletUnlocked,
pub provider: Provider,
pub datatype: PhantomData<D>,
pub log_decoder: LogDecoder,
}
impl<D> ContractCallHandler<D>
where
D: Tokenizable + Debug,
{
pub fn set_contracts(mut self, contract_ids: &[Bech32ContractId]) -> Self {
self.contract_call.external_contracts = contract_ids.to_vec();
self
}
pub fn append_contract(mut self, contract_id: Bech32ContractId) -> Self {
self.contract_call.append_external_contracts(contract_id);
self
}
pub fn tx_params(mut self, params: TxParameters) -> Self {
self.tx_parameters = params;
self
}
pub fn call_params(mut self, params: CallParameters) -> Self {
self.contract_call.call_parameters = params;
self
}
pub fn append_variable_outputs(mut self, num: u64) -> Self {
self.contract_call.append_variable_outputs(num);
self
}
pub fn append_message_outputs(mut self, num: u64) -> Self {
self.contract_call.append_message_outputs(num);
self
}
async fn call_or_simulate(&self, simulate: bool) -> Result<FuelCallResponse<D>, Error> {
let script = self.get_executable_call().await?;
let receipts = if simulate {
script.simulate(&self.provider).await?
} else {
script.execute(&self.provider).await?
};
self.get_response(receipts)
}
pub async fn get_executable_call(&self) -> Result<ExecutableFuelCall, Error> {
ExecutableFuelCall::from_contract_calls(
std::slice::from_ref(&self.contract_call),
&self.tx_parameters,
&self.wallet,
)
.await
}
pub async fn call(self) -> Result<FuelCallResponse<D>, Error> {
Self::call_or_simulate(&self, false)
.await
.map_err(|err| decode_revert_error(err, &self.log_decoder))
}
pub async fn simulate(self) -> Result<FuelCallResponse<D>, Error> {
Self::call_or_simulate(&self, true)
.await
.map_err(|err| decode_revert_error(err, &self.log_decoder))
}
async fn simulate_without_decode(&self) -> Result<(), Error> {
let script = self.get_executable_call().await?;
let provider = self.wallet.get_provider()?;
script.simulate(provider).await?;
Ok(())
}
pub async fn estimate_tx_dependencies(
mut self,
max_attempts: Option<u64>,
) -> Result<Self, Error> {
let attempts = max_attempts.unwrap_or(DEFAULT_TX_DEP_ESTIMATION_ATTEMPTS);
for _ in 0..attempts {
let result = self.simulate_without_decode().await;
match result {
Err(Error::RevertTransactionError(_, receipts))
if ContractCall::is_missing_output_variables(&receipts) =>
{
self = self.append_variable_outputs(1);
}
Err(Error::RevertTransactionError(_, ref receipts)) => {
if let Some(receipt) = ContractCall::find_contract_not_in_inputs(receipts) {
let contract_id = Bech32ContractId::from(*receipt.contract_id().unwrap());
self = self.append_contract(contract_id);
} else {
return Err(result.expect_err("Couldn't estimate tx dependencies because we couldn't find the missing contract input"));
}
}
Err(e) => return Err(e),
_ => return Ok(self),
}
}
match self.call_or_simulate(true).await {
Ok(_) => Ok(self),
Err(e) => Err(e),
}
}
pub async fn estimate_transaction_cost(
&self,
tolerance: Option<f64>,
) -> Result<TransactionCost, Error> {
let script = self.get_executable_call().await?;
let transaction_cost = self
.provider
.estimate_transaction_cost(&script.tx, tolerance)
.await?;
Ok(transaction_cost)
}
pub fn get_response(&self, mut receipts: Vec<Receipt>) -> Result<FuelCallResponse<D>, Error> {
let token = get_decoded_output(
&mut receipts,
Some(&self.contract_call.contract_id),
&self.contract_call.output_param,
)?;
Ok(FuelCallResponse::new(
D::from_token(token)?,
receipts,
self.log_decoder.clone(),
))
}
}
#[derive(Debug)]
#[must_use = "contract calls do nothing unless you `call` them"]
pub struct MultiContractCallHandler {
pub contract_calls: Vec<ContractCall>,
pub log_decoder: LogDecoder,
pub tx_parameters: TxParameters,
pub wallet: WalletUnlocked,
}
impl MultiContractCallHandler {
pub fn new(wallet: WalletUnlocked) -> Self {
Self {
contract_calls: vec![],
tx_parameters: TxParameters::default(),
wallet,
log_decoder: LogDecoder {
logs_map: HashMap::new(),
},
}
}
pub fn add_call<D: Tokenizable>(&mut self, call_handler: ContractCallHandler<D>) -> &mut Self {
self.log_decoder.merge(&call_handler.log_decoder);
self.contract_calls.push(call_handler.contract_call);
self
}
pub fn tx_params(&mut self, params: TxParameters) -> &mut Self {
self.tx_parameters = params;
self
}
pub async fn get_executable_call(&self) -> Result<ExecutableFuelCall, Error> {
if self.contract_calls.is_empty() {
panic!("No calls added. Have you used '.add_calls()'?");
}
ExecutableFuelCall::from_contract_calls(
&self.contract_calls,
&self.tx_parameters,
&self.wallet,
)
.await
}
pub async fn call<D: Tokenizable + Debug>(&self) -> Result<FuelCallResponse<D>, Error> {
Self::call_or_simulate(self, false)
.await
.map_err(|err| decode_revert_error(err, &self.log_decoder))
}
pub async fn simulate<D: Tokenizable + Debug>(&self) -> Result<FuelCallResponse<D>, Error> {
Self::call_or_simulate(self, true)
.await
.map_err(|err| decode_revert_error(err, &self.log_decoder))
}
async fn call_or_simulate<D: Tokenizable + Debug>(
&self,
simulate: bool,
) -> Result<FuelCallResponse<D>, Error> {
let script = self.get_executable_call().await?;
let provider = self.wallet.get_provider()?;
let receipts = if simulate {
script.simulate(provider).await?
} else {
script.execute(provider).await?
};
self.get_response(receipts)
}
async fn simulate_without_decode(&self) -> Result<(), Error> {
let script = self.get_executable_call().await?;
let provider = self.wallet.get_provider()?;
script.simulate(provider).await?;
Ok(())
}
pub async fn estimate_tx_dependencies(
mut self,
max_attempts: Option<u64>,
) -> Result<Self, Error> {
let attempts = max_attempts.unwrap_or(DEFAULT_TX_DEP_ESTIMATION_ATTEMPTS);
for _ in 0..attempts {
let result = self.simulate_without_decode().await;
match result {
Err(Error::RevertTransactionError(_, receipts))
if ContractCall::is_missing_output_variables(&receipts) =>
{
self.contract_calls
.iter_mut()
.take(1)
.for_each(|call| call.append_variable_outputs(1));
}
Err(Error::RevertTransactionError(_, ref receipts)) => {
if let Some(receipt) = ContractCall::find_contract_not_in_inputs(receipts) {
let contract_id = Bech32ContractId::from(*receipt.contract_id().unwrap());
self.contract_calls
.iter_mut()
.take(1)
.for_each(|call| call.append_external_contracts(contract_id.clone()));
} else {
return Err(result.expect_err("Couldn't estimate tx dependencies because we couldn't find the missing contract input"));
}
}
Err(e) => return Err(e),
_ => return Ok(self),
}
}
Ok(self)
}
pub async fn estimate_transaction_cost(
&self,
tolerance: Option<f64>,
) -> Result<TransactionCost, Error> {
let script = self.get_executable_call().await?;
let transaction_cost = self
.wallet
.get_provider()?
.estimate_transaction_cost(&script.tx, tolerance)
.await?;
Ok(transaction_cost)
}
pub fn get_response<D: Tokenizable + Debug>(
&self,
mut receipts: Vec<Receipt>,
) -> Result<FuelCallResponse<D>, Error> {
let mut final_tokens = vec![];
for call in self.contract_calls.iter() {
let decoded =
get_decoded_output(&mut receipts, Some(&call.contract_id), &call.output_param)?;
final_tokens.push(decoded.clone());
}
let tokens_as_tuple = Token::Tuple(final_tokens);
let response = FuelCallResponse::<D>::new(
D::from_token(tokens_as_tuple)?,
receipts,
self.log_decoder.clone(),
);
Ok(response)
}
}
#[cfg(test)]
mod test {
use fuels_test_helpers::launch_provider_and_get_wallet;
use super::*;
#[tokio::test]
#[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidData(\"json\")")]
async fn deploy_panics_on_non_binary_file() {
let wallet = launch_provider_and_get_wallet().await;
Contract::deploy(
"tests/types/contract_output_test/out/debug/contract_output_test-abi.json",
&wallet,
TxParameters::default(),
StorageConfiguration::default(),
)
.await
.unwrap();
}
#[tokio::test]
#[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidData(\"json\")")]
async fn deploy_with_salt_panics_on_non_binary_file() {
let wallet = launch_provider_and_get_wallet().await;
Contract::deploy_with_parameters(
"tests/types/contract_output_test/out/debug/contract_output_test-abi.json",
&wallet,
TxParameters::default(),
StorageConfiguration::default(),
Salt::default(),
)
.await
.unwrap();
}
}