use crate::abi_decoder::ABIDecoder;
use crate::abi_encoder::ABIEncoder;
use crate::errors::Error;
use crate::parameters::TxParameters;
use crate::script::Script;
use anyhow::Result;
use fuel_asm::Opcode;
use fuel_gql_client::client::FuelClient;
use fuel_tx::{
AssetId, ContractId, Input, Output, Receipt, StorageSlot, Transaction, UtxoId, Witness,
};
use fuel_types::{Bytes32, Immediate12, Salt, Word};
use fuel_vm::consts::{REG_CGAS, REG_RET, REG_ZERO, VM_TX_MEMORY};
use fuel_vm::prelude::Contract as FuelContract;
use fuels_core::{
constants::DEFAULT_COIN_AMOUNT, constants::WORD_SIZE, Detokenize, Selector, Token,
};
use fuels_core::{constants::NATIVE_ASSET_ID, ParamType};
use fuels_signers::provider::Provider;
use fuels_signers::{LocalWallet, Signer};
use std::marker::PhantomData;
#[derive(Debug, Clone, Default)]
pub struct CompiledContract {
pub raw: Vec<u8>,
pub salt: Salt,
}
pub struct Contract {
pub compiled_contract: CompiledContract,
pub wallet: LocalWallet,
}
#[derive(Debug)]
pub struct CallResponse<D> {
pub value: D,
pub receipts: Vec<Receipt>,
}
impl Contract {
pub fn new(compiled_contract: CompiledContract, wallet: LocalWallet) -> Self {
Self {
compiled_contract,
wallet,
}
}
pub fn compute_contract_id(compiled_contract: &CompiledContract) -> ContractId {
let fuel_contract = FuelContract::from(compiled_contract.raw.clone());
let root = fuel_contract.root();
fuel_contract.id(
&compiled_contract.salt,
&root,
&FuelContract::default_state_root(),
)
}
#[allow(clippy::too_many_arguments)] pub async fn call(
contract_id: ContractId,
encoded_selector: Option<Selector>,
encoded_args: Option<Vec<u8>>,
fuel_client: &FuelClient,
tx_parameters: TxParameters,
maturity: Word,
custom_inputs: bool,
external_contracts: Option<Vec<ContractId>>,
wallet: LocalWallet,
) -> Result<Vec<Receipt>, Error> {
let script_len = 16;
let script_data_offset = VM_TX_MEMORY + Transaction::script_offset() + script_len;
let script_data_offset = script_data_offset as Immediate12;
#[allow(clippy::iter_cloned_collect)]
let script = vec![
Opcode::ADDI(0x10, REG_ZERO, script_data_offset),
Opcode::CALL(0x10, REG_ZERO, 0x10, REG_CGAS),
Opcode::RET(REG_RET),
Opcode::NOOP,
]
.iter()
.copied()
.collect::<Vec<u8>>();
assert_eq!(script.len(), script_len, "Script length *must* be 16");
let mut script_data: Vec<u8> = vec![];
script_data.extend(contract_id.as_ref());
if let Some(e) = encoded_selector {
script_data.extend(e)
}
if custom_inputs {
let call_data_offset = script_data_offset as usize + ContractId::LEN + 2 * WORD_SIZE;
let call_data_offset = call_data_offset as Word;
script_data.extend(&call_data_offset.to_be_bytes());
}
if let Some(e) = encoded_args {
script_data.extend(e)
}
let mut inputs: Vec<Input> = vec![];
let mut outputs: Vec<Output> = vec![];
let self_contract_input = Input::contract(
UtxoId::new(Bytes32::zeroed(), 0),
Bytes32::zeroed(),
Bytes32::zeroed(),
contract_id,
);
inputs.push(self_contract_input);
let spendables = wallet
.get_spendable_coins(&AssetId::default(), DEFAULT_COIN_AMOUNT as u64)
.await
.unwrap();
for coin in spendables {
let input_coin = Input::coin(
UtxoId::from(coin.utxo_id),
coin.owner.into(),
coin.amount.0,
AssetId::default(),
0,
0,
vec![],
vec![],
);
inputs.push(input_coin);
}
let n_inputs = inputs.len();
let self_contract_output = Output::contract(0, Bytes32::zeroed(), Bytes32::zeroed());
outputs.push(self_contract_output);
let change_output = Output::change(wallet.address(), 0, AssetId::default());
outputs.push(change_output);
if let Some(external_contract_ids) = external_contracts {
for (idx, external_contract_id) in external_contract_ids.iter().enumerate() {
let output_index: u8 = (idx + n_inputs) as u8;
let external_contract_input = Input::contract(
UtxoId::new(Bytes32::zeroed(), output_index),
Bytes32::zeroed(),
Bytes32::zeroed(),
*external_contract_id,
);
inputs.push(external_contract_input);
let external_contract_output =
Output::contract(output_index, Bytes32::zeroed(), Bytes32::zeroed());
outputs.push(external_contract_output);
}
}
let mut tx = Transaction::script(
tx_parameters.gas_price,
tx_parameters.gas_limit,
tx_parameters.byte_price,
maturity,
script,
script_data,
inputs,
outputs,
vec![Witness::from(vec![0u8, 0u8])],
);
wallet.sign_transaction(&mut tx).await?;
let script = Script::new(tx);
script.call(fuel_client).await
}
pub fn method_hash<D: Detokenize>(
provider: &Provider,
contract_id: ContractId,
wallet: &LocalWallet,
signature: Selector,
output_params: &[ParamType],
args: &[Token],
) -> Result<ContractCall<D>, Error> {
let mut encoder = ABIEncoder::new();
let encoded_args = encoder.encode(args).unwrap();
let encoded_selector = signature;
let params = TxParameters::default();
let custom_inputs = args.iter().any(|t| matches!(t, Token::Struct(_)));
let maturity = 0;
Ok(ContractCall {
contract_id,
encoded_args,
tx_parameters: params,
maturity,
encoded_selector,
fuel_client: provider.client.clone(),
datatype: PhantomData,
output_params: output_params.to_vec(),
custom_inputs,
external_contracts: None,
wallet: wallet.clone(),
})
}
pub async fn deploy(
compiled_contract: &CompiledContract,
provider: &Provider,
wallet: &LocalWallet,
params: TxParameters,
) -> Result<ContractId, Error> {
let (mut tx, contract_id) =
Self::contract_deployment_transaction(compiled_contract, wallet, params).await?;
wallet.sign_transaction(&mut tx).await?;
match provider.client.submit(&tx).await {
Ok(_) => Ok(contract_id),
Err(e) => Err(Error::TransactionError(e.to_string())),
}
}
pub fn load_sway_contract(binary_filepath: &str, salt: Salt) -> Result<CompiledContract> {
let bin = std::fs::read(binary_filepath)?;
Ok(CompiledContract { raw: bin, salt })
}
pub async fn contract_deployment_transaction(
compiled_contract: &CompiledContract,
wallet: &LocalWallet,
params: TxParameters,
) -> Result<(Transaction, ContractId), Error> {
let maturity = 0;
let bytecode_witness_index = 0;
let storage_slots: Vec<StorageSlot> = vec![];
let witnesses = vec![compiled_contract.raw.clone().into()];
let static_contracts = vec![];
let contract_id = Self::compute_contract_id(compiled_contract);
let outputs: Vec<Output> = vec![
Output::contract_created(contract_id, FuelContract::default_state_root()),
Output::change(wallet.address(), 0, AssetId::from(NATIVE_ASSET_ID)),
];
let inputs = wallet
.get_asset_inputs_for_amount(AssetId::default(), DEFAULT_COIN_AMOUNT)
.await?;
let tx = Transaction::create(
params.gas_price,
params.gas_limit,
params.byte_price,
maturity,
bytecode_witness_index,
compiled_contract.salt,
static_contracts,
storage_slots,
inputs,
outputs,
witnesses,
);
Ok((tx, contract_id))
}
}
#[derive(Debug)]
#[must_use = "contract calls do nothing unless you `call` them"]
pub struct ContractCall<D> {
pub fuel_client: FuelClient,
pub encoded_args: Vec<u8>,
pub encoded_selector: Selector,
pub contract_id: ContractId,
pub tx_parameters: TxParameters,
pub maturity: u64,
pub datatype: PhantomData<D>,
pub output_params: Vec<ParamType>,
pub custom_inputs: bool,
pub wallet: LocalWallet,
external_contracts: Option<Vec<ContractId>>,
}
impl<D> ContractCall<D>
where
D: Detokenize,
{
pub fn set_contracts(mut self, contract_ids: &[ContractId]) -> Self {
self.external_contracts = Some(contract_ids.to_vec());
self
}
pub fn tx_params(mut self, params: TxParameters) -> Self {
self.tx_parameters = params;
self
}
pub async fn call(self) -> Result<CallResponse<D>, Error> {
let mut receipts = Contract::call(
self.contract_id,
Some(self.encoded_selector),
Some(self.encoded_args),
&self.fuel_client,
self.tx_parameters,
self.maturity,
self.custom_inputs,
self.external_contracts,
self.wallet,
)
.await?;
if self.output_params.is_empty() {
return Ok(CallResponse {
value: D::from_tokens(vec![])?,
receipts,
});
}
let output_param = &self.output_params[0];
let (encoded_value, index) = match output_param.bigger_than_word() {
true => match receipts.iter().find(|&receipt| receipt.data().is_some()) {
Some(r) => {
let index = receipts.iter().position(|elt| elt == r).unwrap();
(r.data().unwrap().to_vec(), Some(index))
}
None => (vec![], None),
},
false => match receipts.iter().find(|&receipt| receipt.val().is_some()) {
Some(r) => {
let index = receipts.iter().position(|elt| elt == r).unwrap();
(r.val().unwrap().to_be_bytes().to_vec(), Some(index))
}
None => (vec![], None),
},
};
if index.is_some() {
receipts.remove(index.unwrap());
}
let mut decoder = ABIDecoder::new();
let decoded_value = decoder.decode(&self.output_params, &encoded_value)?;
Ok(CallResponse {
value: D::from_tokens(decoded_value)?,
receipts,
})
}
}