use crate::abi_decoder::ABIDecoder;
use crate::abi_encoder::ABIEncoder;
use crate::errors::Error;
use crate::script::Script;
use forc::test::{forc_build, BuildCommand};
use fuel_asm::Opcode;
use fuel_gql_client::client::FuelClient;
use fuel_tx::{
Address, Color, 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::ParamType;
use fuels_core::{Detokenize, Selector, Token, WORD_SIZE};
use rand::{Rng, RngCore};
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,
}
#[derive(Debug)]
pub struct CallResponse<D> {
pub value: D,
pub receipts: Vec<Receipt>,
}
impl Contract {
pub fn new(compiled_contract: CompiledContract) -> Self {
Self { compiled_contract }
}
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(),
)
}
pub async fn call(
contract_id: ContractId,
encoded_selector: Option<Selector>,
encoded_args: Option<Vec<u8>>,
fuel_client: &FuelClient,
gas_price: Word,
gas_limit: Word,
byte_price: Word,
maturity: Word,
custom_inputs: bool,
) -> 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;
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!(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 input = Input::contract(
UtxoId::new(Bytes32::zeroed(), 0),
Bytes32::zeroed(),
Bytes32::zeroed(),
contract_id,
);
let output = Output::contract(0, Bytes32::zeroed(), Bytes32::zeroed());
let mut rng = rand::thread_rng();
let random_coin = Input::coin(
UtxoId::new(Bytes32::from(rng.gen::<[u8; 32]>()), 0),
Address::from(rng.gen::<[u8; 32]>()),
rng.next_u64(),
Color::from([0u8; 32]),
0,
0,
vec![],
vec![],
);
let tx = Transaction::script(
gas_price,
gas_limit,
byte_price,
maturity,
script,
script_data,
vec![input, random_coin],
vec![output],
vec![Witness::from(vec![0u8, 0u8])],
);
let script = Script::new(tx);
script.call(fuel_client).await
}
pub fn method_hash<D: Detokenize>(
fuel_client: &FuelClient,
contract_id: ContractId,
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 gas_price = 0;
let byte_price = 0;
let gas_limit = 1_000_000;
let maturity = 0;
let custom_inputs = args.iter().any(|t| matches!(t, Token::Struct(_)));
Ok(ContractCall {
contract_id,
encoded_args,
gas_price,
gas_limit,
byte_price,
maturity,
encoded_selector,
fuel_client: fuel_client.clone(),
datatype: PhantomData,
output_params: output_params.to_vec(),
custom_inputs,
})
}
pub async fn deploy(
compiled_contract: &CompiledContract,
fuel_client: &FuelClient,
) -> Result<ContractId, Error> {
let (tx, contract_id) = Self::contract_deployment_transaction(compiled_contract);
match fuel_client.submit(&tx).await {
Ok(_) => Ok(contract_id),
Err(e) => Err(Error::TransactionError(e.to_string())),
}
}
pub fn compile_sway_contract(
project_path: &str,
salt: Salt,
) -> Result<CompiledContract, Error> {
let build_command = BuildCommand {
debug_outfile: None,
minify_json_abi: false,
path: Some(project_path.into()),
print_finalized_asm: false,
print_intermediate_asm: false,
binary_outfile: None,
offline_mode: false,
output_directory: None,
silent_mode: true,
print_ir: false,
use_ir: false,
};
let (raw, _) =
forc_build::build(build_command).map_err(|message| Error::CompilationError(message))?;
Ok(CompiledContract { salt, raw })
}
pub fn contract_deployment_transaction(
compiled_contract: &CompiledContract,
) -> (Transaction, ContractId) {
let gas_price = 0;
let byte_price = 0;
let gas_limit = 1000000;
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 output = Output::contract_created(contract_id, FuelContract::default_state_root());
let tx = Transaction::create(
gas_price,
gas_limit,
byte_price,
maturity,
bytecode_witness_index,
compiled_contract.salt,
static_contracts,
storage_slots,
vec![],
vec![output],
witnesses,
);
(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 gas_price: u64,
pub gas_limit: u64,
pub maturity: u64,
pub byte_price: u64,
pub datatype: PhantomData<D>,
pub output_params: Vec<ParamType>,
pub custom_inputs: bool,
}
impl<D> ContractCall<D>
where
D: Detokenize,
{
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.gas_price,
self.gas_limit,
self.byte_price,
self.maturity,
self.custom_inputs,
)
.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,
})
}
}