Repository: FuelLabs/fuels-rs (Rust)

Issue:
Call and send to contract
There should be a distinction between calling a contract, which dry runs essentially, and sending to a contract, which actually sends a valid tx to be included on-chain. The former can be used for e.g. view or pure functions,and does not require the tx to be unique since it's never included on-chain. The latter can be used for stateful writes, and requires the tx to be unique in some way.

Names subject to change, not sure if "call" and "send" are the best names, but IIRC this is what ethers uses.

If a notion of state mutability is added to the ABI, then the SDK could actually warn if you attempt to call a method that mutates the state.


Discussion:
We discussed this previously here https://github.com/FuelLabs/fuels-rs/issues/90 (and the issue linked in it). IIRC, the summary of that discussion was that we either:

1. Let the user decide whether to use `call()` or `send()`;
2. Have the compiler output, in the ABI, the state mutability of each method.

(2) is the best but it seems like a major effort in the compiler. Maybe we could start with (1) for now and then, once the compiler implements that, we can do (2)?
(2) alone is not sufficient because sometimes the user will want to dry-run a tx without persistently modifying state.
That's true. So I guess in the end it'll be a mix of both. And (2) will help the SDK detect bad cases (calling `.call()` on a method that mutates state as you mentioned).



Patch:
diff --git a/README.md b/README.md
index 57457cf904..72d17aa798 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,10 @@ Rust SDK for Fuel. It can be used for a variety of things, including but not lim
 - Generating type-safe Rust bindings of contract methods;
 - And more, `fuels-rs` is still in active development.
 
+## Documentation
+
+See [the `fuels-rs` book](https://fuellabs.github.io/fuels-rs/latest/)
+
 ## Features
 
 - [x] Launch Fuel nodes
diff --git a/fuels-contract/src/contract.rs b/fuels-contract/src/contract.rs
index fa60609c73..7eb7a23544 100644
--- a/fuels-contract/src/contract.rs
+++ b/fuels-contract/src/contract.rs
@@ -63,24 +63,17 @@ impl Contract {
         )
     }
 
-    /// Calls an already-deployed contract code.
-    /// Note that this is a "generic" call to a contract
-    /// and it doesn't, yet, call a specific ABI function in that contract.
-    /// We need a wallet to pay for the transaction fees (even though they are 0 right now)
-    #[allow(clippy::too_many_arguments)] // We need that many arguments for now
-    pub async fn call(
-        contract_id: ContractId,
-        encoded_selector: Option<Selector>,
-        encoded_args: Option<Vec<u8>>,
-        fuel_client: &FuelClient,
-        tx_parameters: TxParameters,
-        call_parameters: CallParameters,
-        variable_outputs: Option<Vec<Output>>,
-        maturity: Word,
+    /// Given the necessary arguments, create a script that will be submitted to the node to call
+    /// the contract. The script is the actual opcodes used to call the contract, and the script
+    /// data is for instance the function selector. (script, script_data) is returned as a tuple
+    /// of hex-encoded value vectors
+    pub fn build_script(
+        contract_id: &ContractId,
+        encoded_selector: &Option<Selector>,
+        encoded_args: &Option<Vec<u8>>,
+        call_parameters: &CallParameters,
         compute_calldata_offset: bool,
-        external_contracts: Option<Vec<ContractId>>,
-        wallet: LocalWallet,
-    ) -> Result<Vec<Receipt>, Error> {
+    ) -> Result<(Vec<u8>, Vec<u8>), Error> {
         // Script to call the contract.
         // We use the Opcode to call a contract: `CALL` pointing at the
         // following registers;
@@ -114,6 +107,7 @@ impl Contract {
             ]
         );
 
+        #[allow(clippy::iter_cloned_collect)]
         let script = script.iter().copied().collect::<Vec<u8>>();
 
         // `script_data` consists of:
@@ -159,7 +153,33 @@ impl Contract {
         if let Some(e) = encoded_args {
             script_data.extend(e)
         }
+        Ok((script, script_data))
+    }
 
+    /// Calls a contract method with the given ABI function.
+    /// The wallet is here to pay for the transaction fees (even though they are 0 right now)
+    #[allow(clippy::too_many_arguments)] // We need that many arguments for now
+    async fn call(
+        contract_id: ContractId,
+        encoded_selector: Option<Selector>,
+        encoded_args: Option<Vec<u8>>,
+        fuel_client: &FuelClient,
+        tx_parameters: TxParameters,
+        call_parameters: CallParameters,
+        variable_outputs: Option<Vec<Output>>,
+        maturity: Word,
+        compute_calldata_offset: bool,
+        external_contracts: Option<Vec<ContractId>>,
+        wallet: LocalWallet,
+        simulate: bool,
+    ) -> Result<Vec<Receipt>, Error> {
+        let (script, script_data) = Self::build_script(
+            &contract_id,
+            &encoded_selector,
+            &encoded_args,
+            &call_parameters,
+            compute_calldata_offset,
+        )?;
         let mut inputs: Vec<Input> = vec![];
         let mut outputs: Vec<Output> = vec![];
 
@@ -225,17 +245,17 @@ impl Contract {
                 // output index (TXO). We add the `n_inputs` offset because we added some inputs
                 // above.
                 let output_index: u8 = (idx + n_inputs) as u8;
+                let zeroes = Bytes32::zeroed();
                 let external_contract_input = Input::contract(
                     UtxoId::new(Bytes32::zeroed(), output_index),
-                    Bytes32::zeroed(),
-                    Bytes32::zeroed(),
+                    zeroes,
+                    zeroes,
                     *external_contract_id,
                 );
 
                 inputs.push(external_contract_input);
 
-                let external_contract_output =
-                    Output::contract(output_index, Bytes32::zeroed(), Bytes32::zeroed());
+                let external_contract_output = Output::contract(output_index, zeroes, zeroes);
 
                 outputs.push(external_contract_output);
             }
@@ -261,6 +281,9 @@ impl Contract {
 
         let script = Script::new(tx);
 
+        if simulate {
+            return script.simulate(fuel_client).await;
+        }
         script.call(fuel_client).await
     }
 
@@ -369,7 +392,7 @@ impl Contract {
             // Note that the change will be computed by the node.
             // Here we only have to tell the node who will own the change and its asset ID.
             // For now we use the NATIVE_ASSET_ID constant
-            Output::change(wallet.address(), 0, AssetId::from(NATIVE_ASSET_ID)),
+            Output::change(wallet.address(), 0, NATIVE_ASSET_ID),
         ];
 
         // The first witness is the bytecode we're deploying.
@@ -474,14 +497,14 @@ where
         self
     }
 
-    /// Call a contract's method. Return a Result<CallResponse, Error>.
-    /// The CallResponse structs contains the method's value in its `value`
-    /// field as an actual typed value `D` (if your method returns `bool`, it will
-    /// be a bool, works also for structs thanks to the `abigen!()`).
-    /// The other field of CallResponse, `receipts`, contains the receipts of the
-    /// transaction
-    pub async fn call(self) -> Result<CallResponse<D>, Error> {
-        let mut receipts = Contract::call(
+    /// Call a contract's method on the node. If `simulate==true`, then the call is done in a
+    /// read-only manner, using a `dry-run`. Return a Result<CallResponse, Error>. The CallResponse
+    /// struct contains the method's value in its `value` field as an actual typed value `D` (if
+    /// your method returns `bool`, it will be a bool, works also for structs thanks to the
+    /// `abigen!()`). The other field of CallResponse, `receipts`, contains the receipts of the
+    /// transaction.
+    async fn call_or_simulate(self, simulate: bool) -> Result<CallResponse<D>, Error> {
+        let receipts = Contract::call(
             self.contract_id,
             Some(self.encoded_selector),
             Some(self.encoded_args),
@@ -493,6 +516,7 @@ where
             self.compute_calldata_offset,
             self.external_contracts,
             self.wallet,
+            simulate,
         )
         .await?;
 
@@ -504,10 +528,34 @@ where
             });
         }
 
+        let (decoded_value, receipts) = Self::get_decoded_output(receipts, &self.output_params)?;
+        Ok(CallResponse {
+            value: D::from_tokens(decoded_value)?,
+            receipts,
+        })
+    }
+
+    /// Call a contract's method on the node, in a state-modifying manner.
+    pub async fn call(self) -> Result<CallResponse<D>, Error> {
+        Ok(Self::call_or_simulate(self, false).await?)
+    }
+
+    /// Call a contract's method on the node, in a simulated manner, meaning the state of the
+    /// blockchain is *not* modified but simulated.
+    /// It is the same as the `call` method because the API is more user-friendly this way.
+    pub async fn simulate(self) -> Result<CallResponse<D>, Error> {
+        Ok(Self::call_or_simulate(self, true).await?)
+    }
+
+    /// Based on the returned Contract's output_params and the receipts returned from the call,
+    /// decode the values and return them.
+    pub fn get_decoded_output(
+        mut receipts: Vec<Receipt>,
+        output_params: &[ParamType],
+    ) -> Result<(Vec<Token>, Vec<Receipt>), Error> {
         // Right now we only support methods with a single return type.
         // Soon we'll support tuple as a return type and we'll have to update the logic in here.
-        let output_param = &self.output_params[0];
-
+        let output_param = output_params[0].clone();
         // If the method's return type is bigger than a single `WORD`, the returned value
         // is stored in `ReturnData.data`, otherwise, it's stored in `Return.val`.
         // Here we're checking for that.
@@ -527,15 +575,11 @@ where
                 None => (vec![], None),
             },
         };
-
-        if index.is_some() {
-            receipts.remove(index.unwrap());
+        if let Some(i) = index {
+            receipts.remove(i);
         }
         let mut decoder = ABIDecoder::new();
-        let decoded_value = decoder.decode(&self.output_params, &encoded_value)?;
-        Ok(CallResponse {
-            value: D::from_tokens(decoded_value)?,
-            receipts,
-        })
+        let decoded_value = decoder.decode(output_params, &encoded_value)?;
+        Ok((decoded_value, receipts))
     }
 }
diff --git a/fuels-contract/src/parameters.rs b/fuels-contract/src/parameters.rs
index b24fda8974..4e0143d13e 100644
--- a/fuels-contract/src/parameters.rs
+++ b/fuels-contract/src/parameters.rs
@@ -20,7 +20,7 @@ impl CallParameters {
     pub fn new(amount: Option<u64>, asset_id: Option<AssetId>) -> Self {
         Self {
             amount: amount.unwrap_or(0),
-            asset_id: asset_id.unwrap_or_else(|| NATIVE_ASSET_ID.into()),
+            asset_id: asset_id.unwrap_or(NATIVE_ASSET_ID),
         }
     }
 }
@@ -29,7 +29,7 @@ impl Default for CallParameters {
     fn default() -> Self {
         Self {
             amount: 0,
-            asset_id: NATIVE_ASSET_ID.into(),
+            asset_id: NATIVE_ASSET_ID,
         }
     }
 }
diff --git a/fuels-contract/src/script.rs b/fuels-contract/src/script.rs
index 09ef943fd8..e3e6832fb2 100644
--- a/fuels-contract/src/script.rs
+++ b/fuels-contract/src/script.rs
@@ -20,9 +20,9 @@ impl Script {
         Self { tx }
     }
 
+    // Calling the contract executes the transaction, and is thus state-modifying
     pub async fn call(self, fuel_client: &FuelClient) -> Result<Vec<Receipt>, Error> {
         let tx_id = fuel_client.submit(&self.tx).await?.0.to_string();
-
         let receipts = fuel_client.receipts(&tx_id).await?;
         let status = fuel_client.transaction_status(&tx_id).await?;
         match status {
@@ -30,4 +30,11 @@ impl Script {
             _ => Ok(receipts),
         }
     }
+
+    // Simulating a call to the contract means that the actual state of the blockchain is not
+    // modified, it is only simulated using a "dry-run".
+    pub async fn simulate(self, fuel_client: &FuelClient) -> Result<Vec<Receipt>, Error> {
+        let receipts = fuel_client.dry_run(&self.tx).await?;
+        Ok(receipts)
+    }
 }
diff --git a/fuels-core/src/constants.rs b/fuels-core/src/constants.rs
index ef41d8ae60..56a659afd3 100644
--- a/fuels-core/src/constants.rs
+++ b/fuels-core/src/constants.rs
@@ -1,4 +1,5 @@
 use fuel_tx::Word;
+use fuel_types::AssetId;
 
 pub const DEFAULT_GAS_LIMIT: u64 = 1_000_000;
 pub const DEFAULT_GAS_PRICE: u64 = 0;
@@ -16,4 +17,4 @@ pub const DEFAULT_INITIAL_BALANCE: u64 = 1_000_000_000;
 
 // This constant is the bytes representation of the asset ID of
 // Ethereum right now, the "native" token used for gas fees.
-pub const NATIVE_ASSET_ID: [u8; 32] = [0u8; 32];
+pub const NATIVE_ASSET_ID: AssetId = AssetId::new([0u8; 32]);


Test patch:
diff --git a/fuels-abigen-macro/tests/harness.rs b/fuels-abigen-macro/tests/harness.rs
index 816793a30a..c2cd752152 100644
--- a/fuels-abigen-macro/tests/harness.rs
+++ b/fuels-abigen-macro/tests/harness.rs
@@ -909,13 +909,16 @@ async fn multiple_read_calls() {
 
     contract_instance.store(42).call().await.unwrap();
 
-    let stored = contract_instance.read(0).call().await.unwrap();
+    // Use "simulate" because the methods don't actually run a transaction, but just a dry-run
+    // We can notice here that, thanks to this, we don't generate a TransactionId collision,
+    // even if the transactions are theoretically the same.
+    let stored = contract_instance.read(0).simulate().await.unwrap();
 
-    assert!(stored.value == 42);
+    assert_eq!(stored.value, 42);
 
-    let stored = contract_instance.read(0).call().await.unwrap();
+    let stored = contract_instance.read(0).simulate().await.unwrap();
 
-    assert!(stored.value == 42);
+    assert_eq!(stored.value, 42);
 }
 
 #[tokio::test]