Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support returning Vec<> types from contracts #848

Merged
merged 32 commits into from
Mar 13, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
8f87a4f
feat: modifiy single call script to support vector return
iqdecay Feb 15, 2023
ca94c15
feat: allow returning integer vectors from contracts
iqdecay Feb 15, 2023
f1ebb11
test: add test for integer vector output in contracts
iqdecay Feb 15, 2023
45cf5d4
feat: add support for Vec<struct> contract output
iqdecay Feb 16, 2023
94ec5fe
test: add tests for Rust types
iqdecay Feb 16, 2023
e0c1bec
chore: readapt to base PR
iqdecay Feb 16, 2023
12163b3
style: appease clippy
iqdecay Feb 16, 2023
ab446fa
fix: fix some test bugs
iqdecay Feb 16, 2023
7b02e03
Merge branch 'master' into iqdecay/feat-contract-vec-return
iqdecay Feb 22, 2023
0d4876b
chore: add `vector_output` test project to Forc.toml
iqdecay Feb 22, 2023
e443d21
Merge branch 'master' into iqdecay/feat-contract-vec-return
iqdecay Feb 23, 2023
a6e7a08
style: apply suggestions from code review
iqdecay Mar 2, 2023
170fa5a
refactor: implement code review suggestions
iqdecay Mar 2, 2023
54df892
refactor: use functional approach to filtering ReceiptData
iqdecay Mar 2, 2023
b7f489a
chore: remove faulty function
iqdecay Mar 2, 2023
bfdfb53
Merge branch 'master' into iqdecay/feat-contract-vec-return
iqdecay Mar 2, 2023
717544d
bug: fix nested vector detection in `ParamType`s
iqdecay Mar 4, 2023
64976e5
Merge branch 'master' into iqdecay/feat-contract-vec-return
iqdecay Mar 4, 2023
828aeb8
test: implement test to check that we fail on nested vectors
iqdecay Mar 4, 2023
3c6f9a3
style: appease clippy and all formatting gods
iqdecay Mar 4, 2023
40f0fea
refactor: improve recursion performance in nested vector checks
iqdecay Mar 6, 2023
a3992fb
refactor: extract method for contract call length
iqdecay Mar 6, 2023
428d63e
Merge branch 'master' into iqdecay/feat-contract-vec-return
iqdecay Mar 7, 2023
b68bcbd
Merge branch 'master' into iqdecay/feat-contract-vec-return
iqdecay Mar 10, 2023
7ade987
refactor: %s/contains_no_nested_vector/contains_nested_vectors
iqdecay Mar 10, 2023
99d7a5d
fix: remove failing docs examples
iqdecay Mar 10, 2023
3d8b095
refactor: change `contains_nested_vectors` form
iqdecay Mar 10, 2023
d3cd663
Merge branch 'master' into iqdecay/feat-contract-vec-return
iqdecay Mar 10, 2023
dfbc269
style: remove line
iqdecay Mar 10, 2023
8dc977a
style: appease clippy and all formatting gods
iqdecay Mar 10, 2023
98b84e2
refactor: use the `chain!` operator
iqdecay Mar 12, 2023
59477ec
Update packages/fuels-types/src/param_types.rs
iqdecay Mar 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ use proc_macro2::TokenStream;
use quote::{quote, ToTokens};

use crate::{
error::{error, Result},
error::Result,
program_bindings::{
abi_types::{FullABIFunction, FullTypeApplication, FullTypeDeclaration},
resolved_type::{resolve_type, ResolvedType},
resolved_type::resolve_type,
utils::{param_type_calls, Component},
},
utils::safe_ident,
Expand All @@ -26,8 +26,7 @@ impl FunctionGenerator {
pub fn new(fun: &FullABIFunction, shared_types: &HashSet<FullTypeDeclaration>) -> Result<Self> {
let args = function_arguments(fun.inputs(), shared_types)?;

let output_type = resolve_fn_output_type(fun, shared_types)?;

let output_type = resolve_type(fun.output(), shared_types)?;
Ok(Self {
name: fun.name().to_string(),
args,
Expand Down Expand Up @@ -83,21 +82,6 @@ fn function_arguments(
.collect::<Result<_>>()
}

fn resolve_fn_output_type(
function: &FullABIFunction,
shared_types: &HashSet<FullTypeDeclaration>,
) -> Result<ResolvedType> {
let output_type = resolve_type(function.output(), shared_types)?;
if output_type.uses_vectors() {
Err(error!(
"function '{}' contains a vector in its return type. This currently isn't supported.",
iqdecay marked this conversation as resolved.
Show resolved Hide resolved
function.name()
))
} else {
Ok(output_type)
}
}

impl From<&FunctionGenerator> for TokenStream {
fn from(fun: &FunctionGenerator) -> Self {
let name = safe_ident(&fun.name);
Expand Down
13 changes: 1 addition & 12 deletions packages/fuels-code-gen/src/program_bindings/resolved_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ use fuel_abi_types::utils::{
extract_array_len, extract_custom_type_name, extract_generic_name, extract_str_len,
has_tuple_format,
};
use lazy_static::lazy_static;

use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use regex::Regex;

use crate::{
error::{error, Result},
Expand All @@ -33,16 +32,6 @@ impl ResolvedType {
pub fn is_unit(&self) -> bool {
self.type_name.to_string() == "()"
}
// Used to prevent returning vectors until we get
// the compiler support for it.
#[must_use]
pub fn uses_vectors(&self) -> bool {
lazy_static! {
static ref RE: Regex = Regex::new(r"\bVec\b").unwrap();
}
RE.is_match(&self.type_name.to_string())
|| self.generic_params.iter().any(ResolvedType::uses_vectors)
}
}

impl ToTokens for ResolvedType {
Expand Down
29 changes: 27 additions & 2 deletions packages/fuels-core/src/abi_decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,33 @@ impl ABIDecoder {
}
}

fn decode_vector(_param_type: &ParamType, _bytes: &[u8]) -> Result<DecodeResult> {
unimplemented!("Cannot decode Vectors until we get support from the compiler.")
fn decode_vector(param_type: &ParamType, bytes: &[u8]) -> Result<DecodeResult> {
let memory_size = match param_type {
ParamType::Vector(..) => Err(error!(
InvalidData,
"Vectors containing vectors are not supported!"
)),
_ => Ok(param_type.compute_encoding_width()),
iqdecay marked this conversation as resolved.
Show resolved Hide resolved
}
.unwrap();
iqdecay marked this conversation as resolved.
Show resolved Hide resolved
if bytes.len() % memory_size != 0 {
return Err(error!(
InvalidData,
"The bytes provided do not correspond to a Vec<{:?}> got: {:?}", param_type, bytes
));
}
let mut results = vec![];
let mut bytes_read = 0;
while bytes_read < bytes.len() {
let res = Self::decode_param(param_type, skip(bytes, bytes_read)?)?;
bytes_read += res.bytes_read;
results.push(res.token);
}
iqdecay marked this conversation as resolved.
Show resolved Hide resolved

Ok(DecodeResult {
token: Token::Vector(results),
bytes_read,
})
}

fn decode_tuple(param_types: &[ParamType], bytes: &[u8]) -> Result<DecodeResult> {
Expand Down
70 changes: 57 additions & 13 deletions packages/fuels-programs/src/call_utils.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
use itertools::{chain, Itertools};
use std::{collections::HashSet, iter, vec};

use fuel_tx::{
AssetId, Bytes32, ContractId, Input, Output, Receipt, ScriptExecutionResult, TxPointer, UtxoId,
};
use fuel_types::Word;
use fuel_vm::fuel_asm::{op, RegId};

use fuels_core::offsets::call_script_data_offset;
use fuels_signers::{provider::Provider, Signer, WalletUnlocked};
use fuels_types::{
bech32::Bech32Address,
constants::{BASE_ASSET_ID, WORD_SIZE},
errors::{Error, Result},
param_types::ParamType,
parameters::TxParameters,
resource::Resource,
transaction::{ScriptTransaction, Transaction},
};
use itertools::{chain, Itertools};

use crate::contract::ContractCall;

Expand All @@ -38,12 +40,23 @@ pub async fn build_tx_from_contract_calls(
wallet: &WalletUnlocked,
) -> Result<ScriptTransaction> {
let consensus_parameters = wallet.get_provider()?.consensus_parameters().await?;
let n_vectors_calls = calls.iter().filter(|c| c.output_param.is_vector()).count();

// Compute the length of the calling scripts for the two types of contract calls.
// Use placeholder for `call_param_offsets` and `output_param_type`, because the length of the
// calling script doesn't depend on the underlying type, just on whether or not the contract
// call output is a vector.
let calls_instructions_len_no_vectors =
get_single_call_instructions(&CallOpcodeParamsOffset::default(), &ParamType::U64).len()
* (calls.len() - n_vectors_calls);
let calls_instructions_len_vectors = get_single_call_instructions(
&CallOpcodeParamsOffset::default(),
&ParamType::Vector(Box::from(ParamType::U64)),
)
.len()
* n_vectors_calls;

// Calculate instructions length for call instructions
// Use placeholder for call param offsets, we only care about the length
let calls_instructions_len =
get_single_call_instructions(&CallOpcodeParamsOffset::default()).len() * calls.len();

let calls_instructions_len = calls_instructions_len_no_vectors + calls_instructions_len_vectors;
iqdecay marked this conversation as resolved.
Show resolved Hide resolved
let data_offset = call_script_data_offset(&consensus_parameters, calls_instructions_len);

let (script_data, call_param_offsets) =
Expand Down Expand Up @@ -124,11 +137,12 @@ pub(crate) fn get_instructions(
calls: &[ContractCall],
offsets: Vec<CallOpcodeParamsOffset>,
) -> Vec<u8> {
let num_calls = calls.len();

let mut instructions = vec![];
for (_, call_offsets) in (0..num_calls).zip(offsets.iter()) {
instructions.extend(get_single_call_instructions(call_offsets));
for (call, call_offsets) in calls.iter().zip(offsets.iter()) {
instructions.extend(get_single_call_instructions(
call_offsets,
&call.output_param,
));
iqdecay marked this conversation as resolved.
Show resolved Hide resolved
}

instructions.extend(op::ret(RegId::ONE).to_bytes());
Expand Down Expand Up @@ -213,7 +227,10 @@ pub(crate) fn build_script_data_from_contract_calls(
///
/// Note that these are soft rules as we're picking this addresses simply because they
/// non-reserved register.
pub(crate) fn get_single_call_instructions(offsets: &CallOpcodeParamsOffset) -> Vec<u8> {
pub(crate) fn get_single_call_instructions(
offsets: &CallOpcodeParamsOffset,
output_param_type: &ParamType,
) -> Vec<u8> {
let call_data_offset = offsets
.call_data_offset
.try_into()
Expand All @@ -230,15 +247,42 @@ pub(crate) fn get_single_call_instructions(offsets: &CallOpcodeParamsOffset) ->
.asset_id_offset
.try_into()
.expect("asset_id_offset out of range");
let instructions = [

let mut instructions = [
op::movi(0x10, call_data_offset),
op::movi(0x11, gas_forwarded_offset),
op::lw(0x11, 0x11, 0),
op::movi(0x12, amount_offset),
op::lw(0x12, 0x12, 0),
op::movi(0x13, asset_id_offset),
op::call(0x10, 0x12, 0x13, 0x11),
];
]
.to_vec();
// The instructions are different if you want to return data that was on the heap
if output_param_type.is_vector() {
iqdecay marked this conversation as resolved.
Show resolved Hide resolved
if let ParamType::Vector(inner_param_type) = output_param_type {
let inner_type_byte_size: u16 =
(inner_param_type.compute_encoding_width() * WORD_SIZE) as u16;
instructions.extend([
// The RET register contains the pointer address of the `CALL` return (a stack
// address).
// The RETL register contains the length of the `CALL` return (=24 because the vec
// struct takes 3 bytes). We don't actually need it unless the vec struct encoding
iqdecay marked this conversation as resolved.
Show resolved Hide resolved
// changes in the compiler.
// Load the word located at the address contained in RET, it's a word that
// translates to a heap address. 0x15 is a free register.
op::lw(0x15, RegId::RET, 0),
// We know a vector struct has its third byte contain the length of the vector, so
// use a 2 offset to store the vector length in 0x16, which is a free register.
op::lw(0x16, RegId::RET, 2),
// The in-memory size of the vector is (in-memory size of the inner type) * length
op::muli(0x16, 0x16, inner_type_byte_size),
op::retd(0x15, 0x16),
]);
} else {
panic!("Couldn't match to a ParamType::Vector even though `.is_vector()` is true")
}
}

#[allow(clippy::iter_cloned_collect)]
instructions.into_iter().collect::<Vec<u8>>()
Expand Down
42 changes: 41 additions & 1 deletion packages/fuels-programs/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -467,13 +467,53 @@ pub fn get_decoded_output(
contract_id: Option<&Bech32ContractId>,
output_param: &ParamType,
) -> Result<Token> {
let null_contract_id = ContractId::new([0u8; 32]);
// Multiple returns are handled as one `Tuple` (which has its own `ParamType`)
let contract_id: ContractId = match contract_id {
Some(contract_id) => contract_id.into(),
// During a script execution, the script's contract id is the **null** contract id
None => ContractId::new([0u8; 32]),
None => null_contract_id,
};
let encoded_value = match output_param.get_return_location() {
ReturnLocation::ReturnData if output_param.is_vector() => {
// If the output of the function is a vector, then there are 2 consecutive ReturnData
// receipts. The first one is the one that returns the pointer to the vec struct in the
// VM memory, the second one contains the actual vector bytes (that the previous receipt
// points to).
// We ensure to take the right "first" ReturnData receipt by checking for the
// contract_id. There are no receipts in between the two ReturnData receipts because of
// the way the scripts are built (the calling script adds a RETD just after the CALL
// opcode, see `get_single_call_instructions`).
// Find the position of the first corresponding ReturnData receipt
iqdecay marked this conversation as resolved.
Show resolved Hide resolved
let pos = receipts
.iter()
.position(|receipt| {
matches!(receipt,
Receipt::ReturnData { id, data, .. } if *id == contract_id && !data.is_empty())
})
.expect("There should be at least a ReturnData receipt");
// Get the data contained in the one that immediately follows
let return_data_receipt = receipts
.get(pos + 1)
.expect("There should be at least two receipts");
// Its contract id must be null (it comes from a script)
if !matches!(return_data_receipt,
Receipt::ReturnData { id, .. } if *id == null_contract_id)
{
return Err(error!(
InvalidData,
"The next should be a ReturnData receipt with non-empty data and a \
null contract id: {:?}",
return_data_receipt
));
};
Some(
return_data_receipt
.data()
.expect("ReturnData should have data")
.to_vec(),
)
}
iqdecay marked this conversation as resolved.
Show resolved Hide resolved
ReturnLocation::ReturnData => receipts
.iter()
.find(|receipt| {
Expand Down
3 changes: 2 additions & 1 deletion packages/fuels-signers/src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,13 @@ impl Provider {
/// ## Sending a transaction
///
/// ```
/// use std::default::Default;
/// use fuels::tx::Script;
/// use fuels::prelude::*;
/// async fn foo() -> std::result::Result<(), Box<dyn std::error::Error>> {
/// // Setup local test node
/// let (provider, _) = setup_test_provider(vec![], vec![], None, None).await;
/// let tx = Script::default();
/// let tx = ScriptTransaction::new(vec![],vec![], TxParameters::default());
///
/// let receipts = provider.send_transaction(&tx).await?;
/// dbg!(receipts);
Expand Down
4 changes: 4 additions & 0 deletions packages/fuels-types/src/param_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ impl ParamType {
}
}

pub fn is_vector(&self) -> bool {
matches!(self, Self::Vector(..))
}

/// Calculates the number of `WORD`s the VM expects this parameter to be encoded in.
pub fn compute_encoding_width(&self) -> usize {
const fn count_words(bytes: usize) -> usize {
Expand Down
Loading