Skip to content

Commit

Permalink
Add support for contract calls with struct as inputs (#36)
Browse files Browse the repository at this point in the history
* Add support for contract calls with struct as inputs

Before the changes introduced here, using the SDK to call contracts
that takes structs as input would cause weird bugs -- it didn't really
cause a panic on the VM, but it made the access to fields within
a struct retrieve garbage data.

That was happening because in the `script_data` we sent in the tx
we weren't including a `call_data_offset` that's computed as:

```rust
let call_data_offset = script_data_offset as usize
                        + ContractId::LEN + 2 * WORD_SIZE;
```

Once that was included, struct field access worked perfectly.

Curiously, this `call_data_offset` isn't needed if the input is a
primitive type (u8, bool, ...). So a conditional check that adds
the `call_data_offset` to the `script_data` iff the ABI method called
takes a struct is now in place.

This enables users to successfully perform the following:

```Rust
// ...
let contract_instance = MyContract::new(compiled, client);

let counter_config = CounterConfig {
    dummy: true,
    initial_value: 42,
};

let result = contract_instance
    .initialize_counter(counter_config)
    .call()
    .await
    .unwrap();

assert_eq!(42, result);
```

* Tweak calldata offset documentation
  • Loading branch information
digorithm committed Jan 13, 2022
1 parent 838ba1a commit 210a14c
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 4 deletions.
127 changes: 127 additions & 0 deletions fuels-abigen-macro/tests/harness.rs
Expand Up @@ -927,3 +927,130 @@ async fn type_safe_output_values() {

let _response = contract_instance.return_my_struct(my_struct).call().await;
}

#[tokio::test]
async fn call_with_structs() {
let rng = &mut StdRng::seed_from_u64(2322u64);

// Generates the bindings from the an ABI definition inline.
// The generated bindings can be accessed through `MyContract`.
abigen!(
MyContract,
r#"
[
{
"inputs": [
{
"components": null,
"name": "gas_",
"type": "u64"
},
{
"components": null,
"name": "amount_",
"type": "u64"
},
{
"components": null,
"name": "color_",
"type": "b256"
},
{
"components": [
{
"components": null,
"name": "dummy",
"type": "bool"
},
{
"components": null,
"name": "initial_value",
"type": "u64"
}
],
"name": "config",
"type": "struct CounterConfig"
}
],
"name": "initialize_counter",
"outputs": [
{
"components": null,
"name": "",
"type": "u64"
}
],
"type": "function"
},
{
"inputs": [
{
"components": null,
"name": "gas_",
"type": "u64"
},
{
"components": null,
"name": "amount_",
"type": "u64"
},
{
"components": null,
"name": "color_",
"type": "b256"
},
{
"components": null,
"name": "amount",
"type": "u64"
}
],
"name": "increment_counter",
"outputs": [
{
"components": null,
"name": "",
"type": "u64"
}
],
"type": "function"
}
]
"#
);

// Build the contract
let salt: [u8; 32] = rng.gen();
let salt = Salt::from(salt);

let compiled =
Contract::compile_sway_contract("tests/test_projects/complex_types_contract", salt)
.unwrap();

let (client, contract_id) = Contract::launch_and_deploy(&compiled).await.unwrap();

println!("Contract deployed @ {:x}", contract_id);

let contract_instance = MyContract::new(compiled, client);

let counter_config = CounterConfig {
dummy: true,
initial_value: 42,
};

let result = contract_instance
.initialize_counter(counter_config) // Build the ABI call
.call() // Perform the network call
.await
.unwrap();

assert_eq!(42, result);

let result = contract_instance
.increment_counter(10)
.call()
.await
.unwrap();

assert_eq!(52, result);
}
@@ -0,0 +1,9 @@
[project]
author = "Rodrigo Araujo"
license = "MIT"
name = "contract_test"
entry = "main.sw"

[dependencies]
std = { path = "../lib-std" }
core = { path = "../lib-core" }
@@ -0,0 +1,31 @@
contract;

use std::storage::store;
use std::storage::get;
use std::chain::log_u64;
use std::chain::log_u8;

struct CounterConfig {
dummy: bool,
initial_value: u64,
}

abi TestContract {
fn initialize_counter(gas_: u64, amount_: u64, color_: b256, config: CounterConfig) -> u64;
fn increment_counter(gas_: u64, amount_: u64, color_: b256, amount: u64) -> u64;
}

const COUNTER_KEY = 0x0000000000000000000000000000000000000000000000000000000000000000;

impl TestContract for Contract {
fn initialize_counter(gas_: u64, amount_: u64, color_: b256, config: CounterConfig) -> u64 {
let value = config.initial_value;
store(COUNTER_KEY, value);
value
}
fn increment_counter(gas_: u64, amount_: u64, color_: b256, amount: u64) -> u64 {
let value = get::<u64>(COUNTER_KEY) + amount;
store(COUNTER_KEY, value);
value
}
}
33 changes: 29 additions & 4 deletions fuels-rs/src/contract.rs
Expand Up @@ -12,7 +12,7 @@ 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};
use fuels_core::{Detokenize, Selector, Token, WORD_SIZE};
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use std::marker::PhantomData;
Expand Down Expand Up @@ -59,6 +59,7 @@ impl Contract {
gas_price: Word,
gas_limit: Word,
maturity: Word,
custom_inputs: bool,
) -> Result<Vec<Receipt>, String> {
// Based on the defined script length,
// we set the appropriate data offset.
Expand Down Expand Up @@ -87,16 +88,35 @@ impl Contract {
assert!(script.len() == script_len, "Script length *must* be 16");

// `script_data` consists of:
// 1. The contract ID
// 2. The function selector
// 3. The encoded arguments, in order
// 1. Contract ID (ContractID::LEN);
// 2. Function selector (1 * WORD_SIZE);
// 3. Calldata offset, if it has structs as input,
// computed as `script_data_offset` + ContractId::LEN
// + 2 * WORD_SIZE;
// 4. Encoded arguments.
let mut script_data: Vec<u8> = vec![];

// Insert contract_id
script_data.extend(contract_id.as_ref());

// Insert encoded function selector, if any
if let Some(e) = encoded_selector {
script_data.extend(e)
}

// If the method call takes custom inputs, such as structs or enums,
// we need to calculate the `call_data_offset`, which points to
// where the data for the custom types start in the transaction.
// If it doesn't take any custom inputs, this isn't necessary.
if custom_inputs {
// Offset of the script data relative to the call data
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());
}

// Insert encoded arguments, if any
if let Some(e) = encoded_args {
script_data.extend(e)
}
Expand Down Expand Up @@ -162,6 +182,8 @@ impl Contract {
let maturity = 0;
let input_index = 0;

let custom_inputs = args.iter().any(|t| matches!(t, Token::Struct(_)));

Ok(ContractCall {
compiled_contract: compiled_contract.clone(),
contract_id: Self::compute_contract_id(compiled_contract),
Expand All @@ -177,6 +199,7 @@ impl Contract {
fuel_client: fuel_client.clone(),
datatype: PhantomData,
output_params: output_params.to_vec(),
custom_inputs,
})
}

Expand Down Expand Up @@ -297,6 +320,7 @@ pub struct ContractCall<D> {
pub maturity: u64,
pub datatype: PhantomData<D>,
pub output_params: Vec<ParamType>,
pub custom_inputs: bool,
}

impl<D> ContractCall<D>
Expand All @@ -322,6 +346,7 @@ where
self.gas_price,
self.gas_limit,
self.maturity,
self.custom_inputs,
)
.await
.unwrap();
Expand Down
1 change: 1 addition & 0 deletions fuels-rs/tests/calls.rs
Expand Up @@ -100,6 +100,7 @@ async fn contract_call() {
gas_price,
gas_limit,
maturity,
false,
)
.await
.unwrap();
Expand Down

0 comments on commit 210a14c

Please sign in to comment.