fuels_rs/
contract.rs

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
23/// Contract is a struct to interface with a contract. That includes things such as
24/// compiling, deploying, and running transactions against a contract.
25pub 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    /// Calls an already-deployed contract code.
41    /// Note that this is a "generic" call to a contract
42    /// and it doesn't, yet, call a specific ABI function in that contract.
43    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        // Based on the defined script length,
54        // we set the appropriate data offset.
55        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        // Script to call the contract.
60        // The offset that points to the `script_data`
61        // is loaded at the register `0x10`. Note that
62        // we're picking `0x10` simply because
63        // it could be any non-reserved register.
64        // Then, we use the Opcode to call a contract: `CALL`
65        // pointing at the register that we loaded the
66        // `script_data` at.
67        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        // `script_data` consists of:
80        // 1. Contract ID (ContractID::LEN);
81        // 2. Function selector (1 * WORD_SIZE);
82        // 3. Calldata offset, if it has structs as input,
83        // computed as `script_data_offset` + ContractId::LEN
84        //                                  + 2 * WORD_SIZE;
85        // 4. Encoded arguments.
86        let mut script_data: Vec<u8> = vec![];
87
88        // Insert contract_id
89        script_data.extend(contract_id.as_ref());
90
91        // Insert encoded function selector, if any
92        if let Some(e) = encoded_selector {
93            script_data.extend(e)
94        }
95
96        // If the method call takes custom inputs, such as structs or enums,
97        // we need to calculate the `call_data_offset`, which points to
98        // where the data for the custom types start in the transaction.
99        // If it doesn't take any custom inputs, this isn't necessary.
100        if custom_inputs {
101            // Offset of the script data relative to the call data
102            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        // Insert encoded arguments, if any
109        if let Some(e) = encoded_args {
110            script_data.extend(e)
111        }
112
113        // Inputs/outputs
114        let input = Input::contract(
115            UtxoId::new(Bytes32::zeroed(), 0),
116            Bytes32::zeroed(),
117            Bytes32::zeroed(),
118            contract_id,
119        );
120        // TODO: for now, assume there is only a single input and output, so input_index is 0.
121        // This needs to be changed to support multiple contract IDs.
122        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    /// Creates an ABI call based on a function selector and
141    /// the encoding of its call arguments, which is a slice of Tokens.
142    /// It returns a prepared ContractCall that can further be used to
143    /// make the actual transaction.
144    /// This method is the underlying implementation of the functions
145    /// generated from an ABI JSON spec, i.e, this is what's generated:
146    /// quote! {
147    ///     #doc
148    ///     pub fn #name(&self #input) -> #result {
149    ///         Contract::method_hash(#tokenized_signature, #arg)
150    ///     }
151    /// }
152    /// For more details see `code_gen/functions_gen.rs`.
153    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    /// Launches a local `fuel-core` network and deploys a contract to it.
187    /// If you want to deploy a contract against another network of
188    /// your choosing, use the `deploy` function instead.
189    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    /// Deploys a compiled contract to a running node
202    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    /// Compiles a Sway contract
215    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    /// Crafts a transaction used to deploy a contract
237    pub fn contract_deployment_transaction(
238        compiled_contract: &CompiledContract,
239    ) -> (Transaction, ContractId) {
240        // @todo get these configurations from
241        // params of this function.
242        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"]
272/// Helper for managing a transaction before submitting it to a node
273pub 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    /// Call a contract's method. Note that it will return
292    /// the method's value as an actual typed value `D`.
293    /// For instance, if your method returns a `bool`, this will be a
294    /// `Result<bool, Error>`. Also works for structs! If your method
295    /// returns `MyStruct`, `MyStruct` will be generated through the `abigen!()`
296    /// and this will return `Result<MyStruct, Error>`.
297    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}