From fe3e4254bc201388378313b613fa492d2134b194 Mon Sep 17 00:00:00 2001 From: iqdecay Date: Tue, 24 Jan 2023 20:57:45 +0100 Subject: [PATCH] feat: support `raw_slice` returns from scripts (#743) - 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`. --- .gitignore | 5 ++- packages/fuels-core/src/abi_decoder.rs | 27 ++++++++++++ packages/fuels-core/src/abi_encoder.rs | 3 ++ packages/fuels-core/src/function_selector.rs | 1 + .../fuels-core/src/traits/parameterize.rs | 13 +++++- packages/fuels-core/src/traits/tokenizable.rs | 43 ++++++++++++++++++- .../abigen_macro/code_gen/resolved_type.rs | 19 ++++++++ .../src/abigen_macro/code_gen/utils.rs | 1 + packages/fuels-types/src/core.rs | 3 ++ packages/fuels-types/src/core/raw_slice.rs | 22 ++++++++++ packages/fuels-types/src/param_types.rs | 5 +++ packages/fuels/src/lib.rs | 2 +- packages/fuels/tests/contracts.rs | 31 +++++++++++++ .../contracts/contract_raw_slice/Forc.toml | 7 +++ .../contracts/contract_raw_slice/src/main.sw | 16 +++++++ packages/fuels/tests/scripts.rs | 23 ++++++++++ .../tests/scripts/script_raw_slice/Forc.toml | 7 +++ .../scripts/script_raw_slice/src/main.sw | 11 +++++ 18 files changed, 235 insertions(+), 4 deletions(-) create mode 100644 packages/fuels-types/src/core/raw_slice.rs create mode 100644 packages/fuels/tests/contracts/contract_raw_slice/Forc.toml create mode 100644 packages/fuels/tests/contracts/contract_raw_slice/src/main.sw create mode 100644 packages/fuels/tests/scripts/script_raw_slice/Forc.toml create mode 100644 packages/fuels/tests/scripts/script_raw_slice/src/main.sw diff --git a/.gitignore b/.gitignore index 484f0e6e26..532a79a683 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ \ No newline at end of file +packages/fuels/tests/**/**/target/ + +# Don't add .gitignore files from test Sway projects. +packages/fuels/tests/**/**/.gitignore diff --git a/packages/fuels-core/src/abi_decoder.rs b/packages/fuels-core/src/abi_decoder.rs index 2c4192b15a..b91112f831 100644 --- a/packages/fuels-core/src/abi_decoder.rs +++ b/packages/fuels-core/src/abi_decoder.rs @@ -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}, @@ -9,6 +10,8 @@ use fuels_types::{ param_types::ParamType, }; +use crate::Tokenizable; + #[derive(Debug, Clone)] struct DecodeResult { token: Token, @@ -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), } } @@ -123,6 +127,29 @@ impl ABIDecoder { }) } + fn decode_raw_slice(bytes: &[u8]) -> Result { + // A raw slice is actually an array of u64. + let u64_size = std::mem::size_of::(); + 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::, 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 { let encoded_len = padded_len_usize(length); let encoded_str = peek(bytes, encoded_len)?; diff --git a/packages/fuels-core/src/abi_encoder.rs b/packages/fuels-core/src/abi_encoder.rs index 89afb4901f..ba0647bb39 100644 --- a/packages/fuels-core/src/abi_encoder.rs +++ b/packages/fuels-core/src/abi_encoder.rs @@ -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) diff --git a/packages/fuels-core/src/function_selector.rs b/packages/fuels-core/src/function_selector.rs index 18857b4b54..1dac4c785d 100644 --- a/packages/fuels-core/src/function_selector.rs +++ b/packages/fuels-core/src/function_selector.rs @@ -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"), } } diff --git a/packages/fuels-core/src/traits/parameterize.rs b/packages/fuels-core/src/traits/parameterize.rs index 27d6a4ba63..06ed93f701 100644 --- a/packages/fuels-core/src/traits/parameterize.rs +++ b/packages/fuels-core/src/traits/parameterize.rs @@ -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, @@ -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 @@ -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); + } } diff --git a/packages/fuels-core/src/traits/tokenizable.rs b/packages/fuels-core/src/traits/tokenizable.rs index efb10e118c..883fb58bc6 100644 --- a/packages/fuels-core/src/traits/tokenizable.rs +++ b/packages/fuels-core/src/traits/tokenizable.rs @@ -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, @@ -227,6 +229,24 @@ impl Tokenizable for u64 { } } +impl Tokenizable for RawSlice { + fn from_token(token: Token) -> Result + 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 Tokenizable for (T,)`. // So we implement `Tokenizable` for each tuple length, covering @@ -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]; diff --git a/packages/fuels-macros/src/abigen_macro/code_gen/resolved_type.rs b/packages/fuels-macros/src/abigen_macro/code_gen/resolved_type.rs index 3395a8d853..f67a9e9cb2 100644 --- a/packages/fuels-macros/src/abigen_macro/code_gen/resolved_type.rs +++ b/packages/fuels-macros/src/abigen_macro/code_gen/resolved_type.rs @@ -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() @@ -227,6 +228,7 @@ fn to_byte( None } } + fn to_bits256( type_field: &str, _: impl Fn() -> Vec, @@ -243,6 +245,23 @@ fn to_bits256( } } +fn to_raw_slice( + type_field: &str, + _: impl Fn() -> Vec, + _: impl Fn() -> Vec, + _: bool, +) -> Option { + 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, diff --git a/packages/fuels-macros/src/abigen_macro/code_gen/utils.rs b/packages/fuels-macros/src/abigen_macro/code_gen/utils.rs index bff8e4c12f..6b8e679552 100644 --- a/packages/fuels-macros/src/abigen_macro/code_gen/utils.rs +++ b/packages/fuels-macros/src/abigen_macro/code_gen/utils.rs @@ -125,6 +125,7 @@ pub(crate) fn get_sdk_provided_types() -> Vec { "::fuels::types::Identity", "::fuels::types::EvmAddress", "::fuels::types::B512", + "::fuels::types::RawSlice", "::std::vec::Vec", "::std::result::Result", "::std::option::Option", diff --git a/packages/fuels-types/src/core.rs b/packages/fuels-types/src/core.rs index b22a4c957c..0debf429ee 100644 --- a/packages/fuels-types/src/core.rs +++ b/packages/fuels-types/src/core.rs @@ -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]; @@ -79,6 +81,7 @@ pub enum Token { #[strum(disabled)] Enum(Box), Tuple(Vec), + RawSlice(Vec), } impl fmt::Display for Token { diff --git a/packages/fuels-types/src/core/raw_slice.rs b/packages/fuels-types/src/core/raw_slice.rs new file mode 100644 index 0000000000..d4e0b0cd62 --- /dev/null +++ b/packages/fuels-types/src/core/raw_slice.rs @@ -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); + +impl From for Vec { + fn from(raw_slice: RawSlice) -> Vec { + raw_slice.0 + } +} + +impl PartialEq> for RawSlice { + fn eq(&self, other: &Vec) -> bool { + self.0 == *other + } +} + +impl PartialEq for Vec { + fn eq(&self, other: &RawSlice) -> bool { + *self == other.0 + } +} diff --git a/packages/fuels-types/src/param_types.rs b/packages/fuels-types/src/param_types.rs index 161b8dd05c..56141fddf2 100644 --- a/packages/fuels-types/src/param_types.rs +++ b/packages/fuels-types/src/param_types.rs @@ -42,6 +42,7 @@ pub enum ParamType { generics: Vec, }, Tuple(Vec), + RawSlice, } impl Default for ParamType { @@ -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. diff --git a/packages/fuels/src/lib.rs b/packages/fuels/src/lib.rs index 90d8755a75..43c3980072 100644 --- a/packages/fuels/src/lib.rs +++ b/packages/fuels/src/lib.rs @@ -76,7 +76,7 @@ pub mod prelude { types::{ bech32::{Bech32Address, Bech32ContractId}, errors::Error, - Address, AssetId, ContractId, + Address, AssetId, ContractId, RawSlice, }, }; } diff --git a/packages/fuels/tests/contracts.rs b/packages/fuels/tests/contracts.rs index 2c9905f208..2cafd9429c 100644 --- a/packages/fuels/tests/contracts.rs +++ b/packages/fuels/tests/contracts.rs @@ -1,3 +1,4 @@ +#[allow(unused_imports)] use std::future::Future; use fuels::prelude::*; @@ -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::>()); + } + + Ok(()) +} + #[tokio::test] async fn test_deploy_error_messages() -> Result<(), Error> { let wallet = launch_provider_and_get_wallet().await; diff --git a/packages/fuels/tests/contracts/contract_raw_slice/Forc.toml b/packages/fuels/tests/contracts/contract_raw_slice/Forc.toml new file mode 100644 index 0000000000..5725c95ea2 --- /dev/null +++ b/packages/fuels/tests/contracts/contract_raw_slice/Forc.toml @@ -0,0 +1,7 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "contract_raw_slice" + +[dependencies] diff --git a/packages/fuels/tests/contracts/contract_raw_slice/src/main.sw b/packages/fuels/tests/contracts/contract_raw_slice/src/main.sw new file mode 100644 index 0000000000..64d8eaa309 --- /dev/null +++ b/packages/fuels/tests/contracts/contract_raw_slice/src/main.sw @@ -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() + } +} diff --git a/packages/fuels/tests/scripts.rs b/packages/fuels/tests/scripts.rs index ba5ff5eef0..8656b747be 100644 --- a/packages/fuels/tests/scripts.rs +++ b/packages/fuels/tests/scripts.rs @@ -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::>()); + } + Ok(()) +} diff --git a/packages/fuels/tests/scripts/script_raw_slice/Forc.toml b/packages/fuels/tests/scripts/script_raw_slice/Forc.toml new file mode 100644 index 0000000000..5de7d6466e --- /dev/null +++ b/packages/fuels/tests/scripts/script_raw_slice/Forc.toml @@ -0,0 +1,7 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "script_raw_slice" + +[dependencies] diff --git a/packages/fuels/tests/scripts/script_raw_slice/src/main.sw b/packages/fuels/tests/scripts/script_raw_slice/src/main.sw new file mode 100644 index 0000000000..60e955a287 --- /dev/null +++ b/packages/fuels/tests/scripts/script_raw_slice/src/main.sw @@ -0,0 +1,11 @@ +script; + +fn main(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() +}