1use crate::abi_decoder::ABIDecoder;
2use crate::abi_encoder::ABIEncoder;
3use crate::errors::Error;
4use crate::script::Script;
5use forc::test::{forc_build, BuildCommand};
6use fuel_asm::Opcode;
7use fuel_core::service::{Config, FuelService};
8use fuel_gql_client::client::FuelClient;
9use fuel_tx::{ContractId, Input, Output, Receipt, Transaction, UtxoId};
10use fuel_types::{Bytes32, Immediate12, Salt, Word};
11use fuel_vm::consts::{REG_CGAS, REG_RET, REG_ZERO, VM_TX_MEMORY};
12use fuel_vm::prelude::Contract as FuelContract;
13use fuels_core::ParamType;
14use fuels_core::{Detokenize, Selector, Token, WORD_SIZE};
15use std::marker::PhantomData;
16
17#[derive(Debug, Clone, Default)]
18pub struct CompiledContract {
19 pub raw: Vec<u8>,
20 pub salt: Salt,
21}
22
23pub struct Contract {
26 pub compiled_contract: CompiledContract,
27}
28
29impl Contract {
30 pub fn new(compiled_contract: CompiledContract) -> Self {
31 Self { compiled_contract }
32 }
33
34 pub fn compute_contract_id(compiled_contract: &CompiledContract) -> ContractId {
35 let fuel_contract = FuelContract::from(compiled_contract.raw.clone());
36 let root = fuel_contract.root();
37 fuel_contract.id(&compiled_contract.salt, &root)
38 }
39
40 pub async fn call(
44 contract_id: ContractId,
45 encoded_selector: Option<Selector>,
46 encoded_args: Option<Vec<u8>>,
47 fuel_client: &FuelClient,
48 gas_price: Word,
49 gas_limit: Word,
50 maturity: Word,
51 custom_inputs: bool,
52 ) -> Result<Vec<Receipt>, String> {
53 let script_len = 16;
56 let script_data_offset = VM_TX_MEMORY + Transaction::script_offset() + script_len;
57 let script_data_offset = script_data_offset as Immediate12;
58
59 let script = vec![
68 Opcode::ADDI(0x10, REG_ZERO, script_data_offset),
69 Opcode::CALL(0x10, REG_ZERO, 0x10, REG_CGAS),
70 Opcode::RET(REG_RET),
71 Opcode::NOOP,
72 ]
73 .iter()
74 .copied()
75 .collect::<Vec<u8>>();
76
77 assert!(script.len() == script_len, "Script length *must* be 16");
78
79 let mut script_data: Vec<u8> = vec![];
87
88 script_data.extend(contract_id.as_ref());
90
91 if let Some(e) = encoded_selector {
93 script_data.extend(e)
94 }
95
96 if custom_inputs {
101 let call_data_offset = script_data_offset as usize + ContractId::LEN + 2 * WORD_SIZE;
103 let call_data_offset = call_data_offset as Word;
104
105 script_data.extend(&call_data_offset.to_be_bytes());
106 }
107
108 if let Some(e) = encoded_args {
110 script_data.extend(e)
111 }
112
113 let input = Input::contract(
115 UtxoId::new(Bytes32::zeroed(), 0),
116 Bytes32::zeroed(),
117 Bytes32::zeroed(),
118 contract_id,
119 );
120 let output = Output::contract(0, Bytes32::zeroed(), Bytes32::zeroed());
123
124 let tx = Transaction::script(
125 gas_price,
126 gas_limit,
127 maturity,
128 script,
129 script_data,
130 vec![input],
131 vec![output],
132 vec![],
133 );
134
135 let script = Script::new(tx);
136
137 Ok(script.call(fuel_client).await.unwrap())
138 }
139
140 pub fn method_hash<D: Detokenize>(
154 fuel_client: &FuelClient,
155 compiled_contract: &CompiledContract,
156 signature: Selector,
157 output_params: &[ParamType],
158 args: &[Token],
159 ) -> Result<ContractCall<D>, Error> {
160 let mut encoder = ABIEncoder::new();
161
162 let encoded_args = encoder.encode(args).unwrap();
163 let encoded_selector = signature;
164
165 let gas_price = 0;
166 let gas_limit = 1_000_000;
167 let maturity = 0;
168
169 let custom_inputs = args.iter().any(|t| matches!(t, Token::Struct(_)));
170
171 Ok(ContractCall {
172 compiled_contract: compiled_contract.clone(),
173 contract_id: Self::compute_contract_id(compiled_contract),
174 encoded_args,
175 gas_price,
176 gas_limit,
177 maturity,
178 encoded_selector,
179 fuel_client: fuel_client.clone(),
180 datatype: PhantomData,
181 output_params: output_params.to_vec(),
182 custom_inputs,
183 })
184 }
185
186 pub async fn launch_and_deploy(
190 compiled_contract: &CompiledContract,
191 ) -> Result<(FuelClient, ContractId), Error> {
192 let srv = FuelService::new_node(Config::local_node()).await.unwrap();
193
194 let fuel_client = FuelClient::from(srv.bound_address);
195
196 let contract_id = Self::deploy(compiled_contract, &fuel_client).await?;
197
198 Ok((fuel_client, contract_id))
199 }
200
201 pub async fn deploy(
203 compiled_contract: &CompiledContract,
204 fuel_client: &FuelClient,
205 ) -> Result<ContractId, Error> {
206 let (tx, contract_id) = Self::contract_deployment_transaction(compiled_contract);
207
208 match fuel_client.submit(&tx).await {
209 Ok(_) => Ok(contract_id),
210 Err(e) => Err(Error::TransactionError(e.to_string())),
211 }
212 }
213
214 pub fn compile_sway_contract(
216 project_path: &str,
217 salt: Salt,
218 ) -> Result<CompiledContract, Error> {
219 let build_command = BuildCommand {
220 path: Some(project_path.into()),
221 print_finalized_asm: false,
222 print_intermediate_asm: false,
223 binary_outfile: None,
224 offline_mode: false,
225 silent_mode: true,
226 print_ir: false,
227 use_ir: false,
228 };
229
230 let raw =
231 forc_build::build(build_command).map_err(|message| Error::CompilationError(message))?;
232
233 Ok(CompiledContract { salt, raw })
234 }
235
236 pub fn contract_deployment_transaction(
238 compiled_contract: &CompiledContract,
239 ) -> (Transaction, ContractId) {
240 let gas_price = 0;
243 let gas_limit = 1000000;
244 let maturity = 0;
245 let bytecode_witness_index = 0;
246 let witnesses = vec![compiled_contract.raw.clone().into()];
247
248 let static_contracts = vec![];
249
250 let contract_id = Self::compute_contract_id(compiled_contract);
251
252 let output = Output::contract_created(contract_id);
253
254 let tx = Transaction::create(
255 gas_price,
256 gas_limit,
257 maturity,
258 bytecode_witness_index,
259 compiled_contract.salt,
260 static_contracts,
261 vec![],
262 vec![output],
263 witnesses,
264 );
265
266 (tx, contract_id)
267 }
268}
269
270#[derive(Debug)]
271#[must_use = "contract calls do nothing unless you `call` them"]
272pub struct ContractCall<D> {
274 pub fuel_client: FuelClient,
275 pub compiled_contract: CompiledContract,
276 pub encoded_args: Vec<u8>,
277 pub encoded_selector: Selector,
278 pub contract_id: ContractId,
279 pub gas_price: u64,
280 pub gas_limit: u64,
281 pub maturity: u64,
282 pub datatype: PhantomData<D>,
283 pub output_params: Vec<ParamType>,
284 pub custom_inputs: bool,
285}
286
287impl<D> ContractCall<D>
288where
289 D: Detokenize,
290{
291 pub async fn call(self) -> Result<D, Error> {
298 let receipts = Contract::call(
299 self.contract_id,
300 Some(self.encoded_selector),
301 Some(self.encoded_args),
302 &self.fuel_client,
303 self.gas_price,
304 self.gas_limit,
305 self.maturity,
306 self.custom_inputs,
307 )
308 .await
309 .unwrap();
310
311 let returned_value = match Self::get_receipt_value(&receipts) {
312 Some(val) => val.to_be_bytes(),
313 None => [0u8; 8],
314 };
315
316 let mut decoder = ABIDecoder::new();
317
318 let decoded = decoder.decode(&self.output_params, &returned_value)?;
319
320 Ok(D::from_tokens(decoded)?)
321 }
322
323 fn get_receipt_value(receipts: &[Receipt]) -> Option<u64> {
324 for receipt in receipts {
325 if receipt.val().is_some() {
326 return receipt.val();
327 }
328 }
329 None
330 }
331}