Skip to content

Commit

Permalink
feat: support raw_slice returns from scripts (#743)
Browse files Browse the repository at this point in the history
- This PR closes #703 by adding support for returning a `raw untyped slice` in
scripts and contracts.
- This PR maps the Sway type `raw untyped slice` to the SDK type `RawSlice`.
- It only works with `u64` because the memory size of a `u64` is used to decode the returned `raw untyped slice`.
  • Loading branch information
iqdecay authored and MujkicA committed Jan 24, 2023
1 parent 48895d3 commit ecf2d55
Show file tree
Hide file tree
Showing 18 changed files with 235 additions and 4 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,7 @@ packages/fuels/tests/**/**/Forc.lock
packages/fuels/tests/**/**/out/

# Don't add target/ files from test Sway projects.
packages/fuels/tests/**/**/target/
packages/fuels/tests/**/**/target/

# Don't add .gitignore files from test Sway projects.
packages/fuels/tests/**/**/.gitignore
27 changes: 27 additions & 0 deletions packages/fuels-core/src/abi_decoder.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::{convert::TryInto, str};

use fuel_types::bytes::padded_len_usize;
use fuels_types::errors::Error;
use fuels_types::{
constants::WORD_SIZE,
core::{unzip_param_types, StringToken, Token},
Expand All @@ -9,6 +10,8 @@ use fuels_types::{
param_types::ParamType,
};

use crate::Tokenizable;

#[derive(Debug, Clone)]
struct DecodeResult {
token: Token,
Expand Down Expand Up @@ -64,6 +67,7 @@ impl ABIDecoder {
ParamType::Enum { variants, .. } => Self::decode_enum(bytes, variants),
ParamType::Tuple(types) => Self::decode_tuple(types, bytes),
ParamType::Vector(param_type) => Self::decode_vector(param_type, bytes),
ParamType::RawSlice => Self::decode_raw_slice(bytes),
}
}

Expand Down Expand Up @@ -123,6 +127,29 @@ impl ABIDecoder {
})
}

fn decode_raw_slice(bytes: &[u8]) -> Result<DecodeResult, CodecError> {
// A raw slice is actually an array of u64.
let u64_size = std::mem::size_of::<u64>();
if bytes.len() % u64_size != 0 {
return Err(CodecError::InvalidData(format!(
"The bytes provided do not correspond to a raw slice with u64 numbers, got: {:?}",
bytes
)));
}
let u64_length = bytes.len() / u64_size;
let (tokens, bytes_read) = Self::decode_multiple(&vec![ParamType::U64; u64_length], bytes)?;
let elements = tokens
.into_iter()
.map(u64::from_token)
.collect::<Result<Vec<u64>, Error>>()
.map_err(|e| CodecError::InvalidData(e.to_string()))?;

Ok(DecodeResult {
token: Token::RawSlice(elements),
bytes_read,
})
}

fn decode_string(bytes: &[u8], length: usize) -> Result<DecodeResult, CodecError> {
let encoded_len = padded_len_usize(length);
let encoded_str = peek(bytes, encoded_len)?;
Expand Down
3 changes: 3 additions & 0 deletions packages/fuels-core/src/abi_encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ impl ABIEncoder {
Token::Enum(arg_enum) => Self::encode_enum(arg_enum)?,
Token::Tuple(arg_tuple) => Self::encode_tuple(arg_tuple)?,
Token::Unit => vec![Self::encode_unit()],
Token::RawSlice(_) => {
unimplemented!("Encoding a raw_slice is currently not supported by the Fuel spec.")
}
};

Ok(encoded_token)
Expand Down
1 change: 1 addition & 0 deletions packages/fuels-core/src/function_selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ fn resolve_arg(arg: &ParamType) -> String {
let inner = resolve_arg(el_type);
format!("s<{inner}>(s<{inner}>(rawptr,u64),u64)")
}
ParamType::RawSlice => unimplemented!("Raw slices are not supported as arguments"),
}
}

Expand Down
13 changes: 12 additions & 1 deletion packages/fuels-core/src/traits/parameterize.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::iter::zip;

use fuels_types::{
core::{Bits256, Byte, EvmAddress, Identity, SizedAsciiString, B512},
core::{Bits256, Byte, EvmAddress, Identity, RawSlice, SizedAsciiString, B512},
enum_variants::EnumVariants,
param_types::ParamType,
Address, AssetId, ContractId,
Expand Down Expand Up @@ -39,6 +39,12 @@ impl Parameterize for EvmAddress {
}
}

impl Parameterize for RawSlice {
fn param_type() -> ParamType {
ParamType::RawSlice
}
}

impl Parameterize for Byte {
fn param_type() -> ParamType {
ParamType::Byte
Expand Down Expand Up @@ -252,4 +258,9 @@ mod tests {

assert!(matches!(param_type, ParamType::String(3)));
}

#[test]
fn test_param_type_raw_slice() {
assert_eq!(RawSlice::param_type(), ParamType::RawSlice);
}
}
43 changes: 42 additions & 1 deletion packages/fuels-core/src/traits/tokenizable.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use fuels_types::{
core::{Bits256, Byte, EvmAddress, Identity, SizedAsciiString, StringToken, Token, B512},
core::{
Bits256, Byte, EvmAddress, Identity, RawSlice, SizedAsciiString, StringToken, Token, B512,
},
errors::Error,
param_types::ParamType,
Address, AssetId, ContractId,
Expand Down Expand Up @@ -227,6 +229,24 @@ impl Tokenizable for u64 {
}
}

impl Tokenizable for RawSlice {
fn from_token(token: Token) -> Result<Self, Error>
where
Self: Sized,
{
match token {
Token::RawSlice(contents) => Ok(Self(contents)),
_ => Err(Error::InvalidData(format!(
"RawSlice::from_token expected a token of the variant Token::RawSlice, got: {token}"
))),
}
}

fn into_token(self) -> Token {
Token::RawSlice(Vec::from(self))
}
}

// Here we implement `Tokenizable` for a given tuple of a given length.
// This is done this way because we can't use `impl<T> Tokenizable for (T,)`.
// So we implement `Tokenizable` for each tuple length, covering
Expand Down Expand Up @@ -557,6 +577,27 @@ mod tests {
assert_eq!(token, Token::B256(data));
}

#[test]
fn test_from_token_raw_slice() -> Result<(), Error> {
let data = vec![42; 11];
let token = Token::RawSlice(data.clone());

let slice = RawSlice::from_token(token)?;

assert_eq!(slice, data);

Ok(())
}

#[test]
fn test_into_token_raw_slice() {
let data = vec![13; 32];
let raw_slice_token = Token::RawSlice(data.clone());

let token = raw_slice_token.into_token();

assert_eq!(token, Token::RawSlice(data));
}
#[test]
fn test_from_token_evm_addr() -> Result<(), Error> {
let data = [1u8; 32];
Expand Down
19 changes: 19 additions & 0 deletions packages/fuels-macros/src/abigen_macro/code_gen/resolved_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ pub(crate) fn resolve_type(
to_array,
to_sized_ascii_string,
to_tuple,
to_raw_slice,
to_custom_type,
]
.into_iter()
Expand Down Expand Up @@ -227,6 +228,7 @@ fn to_byte(
None
}
}

fn to_bits256(
type_field: &str,
_: impl Fn() -> Vec<ResolvedType>,
Expand All @@ -243,6 +245,23 @@ fn to_bits256(
}
}

fn to_raw_slice(
type_field: &str,
_: impl Fn() -> Vec<ResolvedType>,
_: impl Fn() -> Vec<ResolvedType>,
_: bool,
) -> Option<ResolvedType> {
if type_field == "raw untyped slice" {
let type_name = quote! {::fuels::types::RawSlice};
Some(ResolvedType {
type_name,
generic_params: vec![],
})
} else {
None
}
}

fn to_custom_type(
type_field: &str,
_: impl Fn() -> Vec<ResolvedType>,
Expand Down
1 change: 1 addition & 0 deletions packages/fuels-macros/src/abigen_macro/code_gen/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ pub(crate) fn get_sdk_provided_types() -> Vec<TypePath> {
"::fuels::types::Identity",
"::fuels::types::EvmAddress",
"::fuels::types::B512",
"::fuels::types::RawSlice",
"::std::vec::Vec",
"::std::result::Result",
"::std::option::Option",
Expand Down
3 changes: 3 additions & 0 deletions packages/fuels-types/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ use crate::{enum_variants::EnumVariants, errors::CodecError, param_types::ParamT
mod bits;
mod byte;
mod native;
mod raw_slice;
mod sized_ascii_string;

pub use crate::core::raw_slice::RawSlice;
pub use crate::core::{bits::*, byte::*, native::*, sized_ascii_string::*};

pub type ByteArray = [u8; 8];
Expand Down Expand Up @@ -79,6 +81,7 @@ pub enum Token {
#[strum(disabled)]
Enum(Box<EnumSelector>),
Tuple(Vec<Token>),
RawSlice(Vec<u64>),
}

impl fmt::Display for Token {
Expand Down
22 changes: 22 additions & 0 deletions packages/fuels-types/src/core/raw_slice.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#[derive(Debug, PartialEq, Clone, Eq)]
// `RawSlice` is a mapping of the contract type "untyped raw slice" -- currently the only way of
// returning dynamically sized data from a script.
pub struct RawSlice(pub Vec<u64>);

impl From<RawSlice> for Vec<u64> {
fn from(raw_slice: RawSlice) -> Vec<u64> {
raw_slice.0
}
}

impl PartialEq<Vec<u64>> for RawSlice {
fn eq(&self, other: &Vec<u64>) -> bool {
self.0 == *other
}
}

impl PartialEq<RawSlice> for Vec<u64> {
fn eq(&self, other: &RawSlice) -> bool {
*self == other.0
}
}
5 changes: 5 additions & 0 deletions packages/fuels-types/src/param_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ pub enum ParamType {
generics: Vec<ParamType>,
},
Tuple(Vec<ParamType>),
RawSlice,
}

impl Default for ParamType {
Expand Down Expand Up @@ -96,6 +97,10 @@ impl ParamType {
}
ParamType::Enum { variants, .. } => variants.compute_encoding_width_of_enum(),
ParamType::Tuple(params) => params.iter().map(|p| p.compute_encoding_width()).sum(),
// The ParamType::RawSlice is basically a wrapper around a U8 vector
ParamType::RawSlice => unimplemented!(
"Raw slices are not supported as inputs, so needing the encoding width of a RawSlice should not happen."
),
}
}
/// For when you need to convert a ABI JSON's TypeApplication into a ParamType.
Expand Down
2 changes: 1 addition & 1 deletion packages/fuels/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ pub mod prelude {
types::{
bech32::{Bech32Address, Bech32ContractId},
errors::Error,
Address, AssetId, ContractId,
Address, AssetId, ContractId, RawSlice,
},
};
}
31 changes: 31 additions & 0 deletions packages/fuels/tests/contracts.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#[allow(unused_imports)]
use std::future::Future;

use fuels::prelude::*;
Expand Down Expand Up @@ -944,6 +945,36 @@ async fn test_contract_call_with_non_default_max_input() -> Result<(), Error> {

Ok(())
}

#[tokio::test]
async fn test_contract_raw_slice() -> Result<(), Error> {
let num_wallets = 1;
let num_coins = 1;
let amount = 1000;
let config = WalletsConfig::new(Some(num_wallets), Some(num_coins), Some(amount));

let mut wallets = launch_custom_provider_and_get_wallets(config, None, None).await;
let wallet = wallets.pop().unwrap();
setup_contract_test!(
Abigen(
name = "RawSliceContract",
abi = "packages/fuels/tests/contracts/contract_raw_slice"
),
Deploy(
name = "contract_instance",
contract = "RawSliceContract",
wallet = "wallet"
),
);
let contract_methods = contract_instance.methods();
for length in 0..=10 {
let response = contract_methods.return_raw_slice(length).call().await?;
assert_eq!(response.value, (0..length).collect::<Vec<_>>());
}

Ok(())
}

#[tokio::test]
async fn test_deploy_error_messages() -> Result<(), Error> {
let wallet = launch_provider_and_get_wallet().await;
Expand Down
7 changes: 7 additions & 0 deletions packages/fuels/tests/contracts/contract_raw_slice/Forc.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[project]
authors = ["Fuel Labs <contact@fuel.sh>"]
entry = "main.sw"
license = "Apache-2.0"
name = "contract_raw_slice"

[dependencies]
16 changes: 16 additions & 0 deletions packages/fuels/tests/contracts/contract_raw_slice/src/main.sw
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
contract;
abi RawSliceContract {
fn return_raw_slice(length: u64) -> raw_slice;
}

impl RawSliceContract for Contract {
fn return_raw_slice(length: u64) -> raw_slice {
let mut vec = Vec::new();
let mut counter = 0;
while counter < length {
vec.push(counter);
counter = counter + 1;
}
vec.as_raw_slice()
}
}
23 changes: 23 additions & 0 deletions packages/fuels/tests/scripts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,3 +299,26 @@ async fn test_script_call_with_non_default_max_input() -> Result<(), Error> {

Ok(())
}

#[tokio::test]
async fn test_script_raw_slice() -> Result<(), Error> {
abigen!(Script(
name = "BimBamScript",
abi = "packages/fuels/tests/scripts/script_raw_slice/out/debug/script_raw_slice-abi.json",
));
let num_wallets = 1;
let num_coins = 1;
let amount = 1000;
let config = WalletsConfig::new(Some(num_wallets), Some(num_coins), Some(amount));

let mut wallets = launch_custom_provider_and_get_wallets(config, None, None).await;
let wallet = wallets.pop().unwrap();
let bin_path = "../fuels/tests/scripts/script_raw_slice/out/debug/script_raw_slice.bin";
let instance = BimBamScript::new(wallet.clone(), bin_path);

for length in 0..=10 {
let response = instance.main(length).call().await?;
assert_eq!(response.value, (0..length).collect::<Vec<_>>());
}
Ok(())
}
7 changes: 7 additions & 0 deletions packages/fuels/tests/scripts/script_raw_slice/Forc.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[project]
authors = ["Fuel Labs <contact@fuel.sh>"]
entry = "main.sw"
license = "Apache-2.0"
name = "script_raw_slice"

[dependencies]
Loading

0 comments on commit ecf2d55

Please sign in to comment.